diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b2daee..2cc6fe6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,7 +69,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-24.04] - go: ["1.25.x"] + go: ["1.26.x"] steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/cli-e2e.yml b/.github/workflows/cli-e2e.yml index 423118c..1587382 100644 --- a/.github/workflows/cli-e2e.yml +++ b/.github/workflows/cli-e2e.yml @@ -62,7 +62,7 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: - go-version: "1.25.x" + go-version: "1.26.x" cache: false - name: go vet (cli + internal) run: go vet ./cli/... ./internal/... @@ -77,7 +77,7 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: - go-version: "1.25.x" + go-version: "1.26.x" cache: false - name: build matrix run: bash test/cli/build-matrix.sh diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..871d8a1 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,59 @@ +# Comprehensive programmatic E2E suite (test/e2e). Builds Orva, runs it, and +# drives every domain + the CLI + the AI agentic loop (via a mock LLM — no real +# provider key needed). Scenarios that require sandboxed function invocation +# (real nsjail) self-skip in CI, where nested sandboxing is unavailable. +name: e2e + +on: + push: + branches: [main, dev] + pull_request: + workflow_dispatch: + +jobs: + e2e: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Build orva (server + CLI) + run: make build + + - name: Start Orva + run: | + mkdir -p "$RUNNER_TEMP/orva-data" + ORVA_DATA_DIR="$RUNNER_TEMP/orva-data" ./build/orva serve > "$RUNNER_TEMP/orva.log" 2>&1 & + echo $! > "$RUNNER_TEMP/orva.pid" + # Wait for health (the API/DB come up without nsjail). + for i in $(seq 1 30); do + if curl -fsS http://127.0.0.1:8443/api/v1/system/health >/dev/null 2>&1; then + echo "orva healthy"; break + fi + sleep 1 + done + curl -fsS http://127.0.0.1:8443/api/v1/system/health || (cat "$RUNNER_TEMP/orva.log"; exit 1) + + - name: Run E2E suite + run: | + ADMIN_KEY="$(cat "$RUNNER_TEMP/orva-data/.admin-key")" + python3 test/e2e/run.py --url http://127.0.0.1:8443 --api-key "$ADMIN_KEY" + + - name: Show checklist + if: always() + run: cat test/e2e/CHECKLIST.md || true + + - name: Upload server log + if: always() + uses: actions/upload-artifact@v4 + with: + name: orva-server-log + path: ${{ runner.temp }}/orva.log + if-no-files-found: ignore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d24e0c5..06bec09 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,7 +49,7 @@ jobs: - uses: actions/setup-go@v6 with: - go-version: "1.25.x" + go-version: "1.26.x" # No build cache — every release builds from scratch so we # can't accidentally ship a stale toolchain artifact. cache: false @@ -137,7 +137,7 @@ jobs: - uses: actions/setup-go@v6 with: - go-version: "1.25.x" + go-version: "1.26.x" # No build cache — every release builds from scratch so we # can't accidentally ship a stale toolchain artifact. cache: false diff --git a/.gitignore b/.gitignore index e31a776..046552e 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,7 @@ modelcontextprotocol-sdk-*.tgz orva-backup-* reddit.md **/__pycache__/ +.gstack/ + +# Design-tooling artifacts (impeccable critique snapshots) +.impeccable/ diff --git a/CLAUDE.md b/CLAUDE.md index 9164c54..1e9cdf1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # Orva -Self-hosted Function-as-a-Service (FaaS) for homelab and on-premises use. Users write JavaScript (Node 22/24), Python (3.13/3.14), or TypeScript functions; Orva deploys them into nsjail sandboxes and exposes them over HTTP with a built-in dashboard, CLI, and MCP server. +Self-hosted Function-as-a-Service (FaaS) for homelab and on-premises use. Users write JavaScript (Node 22/24), Python (3.13/3.14), or TypeScript functions; Orva deploys them into nsjail sandboxes and exposes them over HTTP with a built-in dashboard, CLI, MCP server, and an in-product AI chat assistant (the **AI** sidebar section) that operates the instance end-to-end via in-process tool calling (BYO provider keys, embedded Bifrost gateway). ## Quick Start diff --git a/DESIGN.md b/DESIGN.md index 8d43550..460f48c 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -160,6 +160,7 @@ A single muted violet accent on a deep violet-tinted near-black, with cool gray- - **Border** (`#2D2B42`, ≈`oklch(28% 0.045 277)`): every divider, every card outline, every input stroke. The system reads layers through borders, not shadows. - **Foreground** (`#FFFFFF`): currently pure white. **Tint to `#F4F2FA` (≈`oklch(96% 0.01 290)`)** to match the rest of the palette's hue discipline; the eye reads pure white as harsh against a violet-tinted dark. The `--color-foreground-strong: #FFFFFF` token is reserved for text on saturated brand-colour surfaces (primary CTA labels, on-violet badges); the default `--color-foreground` may shift to a tinted off-white in a future palette pass without breaking those high-contrast sites. - **Foreground Muted** (`#A3A3B3`, ≈`oklch(70% 0.018 280)`): all secondary text, icon defaults, table cell content one rank below primary. The cool gray-violet is intentional. +- **Link** (`--color-link: #8b7bd8`): hyperlinks inside rendered prose (the AI chat markdown, Docs). A lighter, less-saturated lift of the violet accent so links read as interactive without competing with primary CTAs. Inline code in the same prose uses `--color-surface-hover` as its chip background. ### Status (semantic, used at low chroma) - **Success** (`#22C55E`, ≈`oklch(73% 0.21 144)`): only for terminal-success states. Pair with the recommended `success/15` tint background and `success/30` border (see Named Rule below). diff --git a/Dockerfile b/Dockerfile index 9ffe4dc..70e167f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN npm ci --no-audit --no-fund COPY frontend/ ./ RUN npm run build -FROM golang:1.25-bookworm AS go +FROM golang:1.26-bookworm AS go WORKDIR /src # go.mod / go.sum live at the repo root since the v2026.05.12 CLI split. COPY go.mod go.sum ./ diff --git a/README.md b/README.md index 4508469..7b06007 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,7 @@ The Build info card at the top of Settings shows the running release's version, | **Version diff** | Side-by-side source diff between any two past deployments — in the dashboard (CodeMirror merge view) or `orva diff ` for git-style unified output in the terminal. | | **MCP server** | 70 tools at `/mcp` — any MCP client (Claude Code, Cursor, etc.) can create functions, deploy code, manage secrets, browse KV, and read logs. | | **OAuth 2.1** | Add Orva as a custom connector in claude.ai or other OAuth-capable MCP clients — no API key copy-paste needed. | +| **Built-in AI assistant** | An in-product agentic chat (the dashboard's **AI** section) that operates your instance end-to-end — create and deploy functions, read logs, manage secrets and routes — using the same tools as the MCP server, in-process. Bring your own provider key (OpenAI, Anthropic, or any OpenAI-compatible endpoint); writes can be gated behind per-conversation approval. | | **16 templates** | Stripe webhooks, GitHub events, JWT auth, OAuth, CSV→JSON, URL shortener, and more — pickable in the editor. | --- @@ -303,6 +304,11 @@ From there an AI agent can create functions, deploy code, invoke them, read logs browse KV state, and pull the full Orva reference docs — all without leaving the chat. Works with Claude Code, Cursor, and any OAuth-capable MCP client like the claude.ai web UI. +Prefer not to wire up an external client? The dashboard has a **built-in AI assistant** (the +**AI** section) that does the same thing in-product: bring your own provider key (OpenAI, +Anthropic, or any OpenAI-compatible endpoint) and chat with an agent that operates your instance +through the same tools, with optional human approval before any write. + --- ## CLI diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md index 0fecc57..54462c5 100644 --- a/backend/CLAUDE.md +++ b/backend/CLAUDE.md @@ -1,6 +1,6 @@ # Backend -Go 1.25. Module: `github.com/Harsh-2002/Orva` (rooted at the repo, not at +Go 1.26 (bumped from 1.25 — the embedded Bifrost AI gateway requires go ≥1.26; the Dockerfile + all CI workflows pin golang 1.26 in lock-step). Module: `github.com/Harsh-2002/Orva` (rooted at the repo, not at `backend/`). SQLite with no CGO (`modernc.org/sqlite`). The server binary built from `backend/cmd/orva` is the daemon (`orva serve`) @@ -53,6 +53,9 @@ go vet ./... | `cli` | Shared `Client` + `Config` for CLI subcommands | | `backup` | `SnapshotDB` / `ArchiveTo` / `RestoreFrom` helpers | | `version` | Single source of truth for the version string | +| `ai` | In-product AI chat assistant. `Manager` (service layer) wires the SQLite store, the secrets cipher (provider-key encryption), the in-process tool registry, the embedded Bifrost LLM gateway (`ai/llm`), and the agentic loop (`ai/agent`). Served at `/api/v1/ai/*` by `server/ai_handler.go` (SSE for chat/approval). The agent's `defaultSystemPrompt` const lives in `ai/manager.go` — it's a Go **raw string, so it must stay backtick-free** (escape any fenced-code examples by description, not literal ```). | +| `ai/llm` | Wraps the embedded Bifrost gateway (`github.com/maximhq/bifrost/core`) behind neutral types + a normalized event stream. `Account` resolves BYO keys from `ai_provider_configs` live. Thinking levels → `ChatReasoning`. | +| `ai/agent` | Agentic loop (≈25-iteration budget): stream model turn → emit SSE deltas → detect tool calls → approval-gate writes → in-process dispatch → feed results back. Decoupled from `mcp` (takes tools + a dispatcher). Two independent gates: the per-conversation **approval policy** (`all_writes` / `destructive_only` / `auto`, checked in `approvalNeeded`) and a separate code-enforced `confirm=true` requirement on destructive tools. | The canonical UUIDv7 generator (`ids`) and HTTP client (`client`) live at **repo-root** `internal/ids/` and `internal/client/` — shared with the slim CLI codebase, not under `backend/internal/`. @@ -74,6 +77,8 @@ Key files: `deploy.go`, `diff.go`, `functions.go`, `invoke.go`, `logs.go`, `cron **Streaming wire protocol**: `response_start` → `chunk` (base64 body data) → `response_end` frames over the worker's stdin/stdout pipe. `proxy.Forward()` owns the write-loop. +**Shared tool registry (single source of truth)**: every operator tool is declared once via `regAddTool` (`mcp/agent_registry.go`). That call registers the tool with BOTH the external MCP server (`server.go`, unchanged behavior) AND the in-process agent registry (`mcp.BuildAgentRegistry(deps, perms)`, gated to the principal's perms). The internal AI agent dispatches these tools directly as Go calls — no MCP transport, no HTTP. Same name/description/schema/destructive-hint feed both fronts, so they never drift. When adding a tool, use `regAddTool(rc, perm, def, handler)` inside a `register*Tools(rc *regCtx)` function — never bare `mcpsdk.AddTool`. + ## Middleware Chain `CORS → BodySizeLimit → Auth → RequestID → Logger → Handler` @@ -90,4 +95,7 @@ SQLite WAL mode. All migrations in `internal/database/migrations.go` — additiv - `execution_requests` has **no FK** to `executions` (intentionally dropped to fix async insert ordering); manual cascade runs in `DeleteExecution`. - TypeScript deploys: after a successful `tsc`, the function's `Entrypoint` is updated to `dist/handler.js` in the DB. The validator on re-deploy checks for the source `.ts` file, not the compiled output. - Zombie nsjail fix: `cmd.Wait()` is centralized in `Spawn` via `waitDone chan struct{}`; never call `Wait()` on the sandbox `cmd` anywhere else. +- **AI conversation editing is destructive-tail:** editing or deleting a chat message (`EditMessage` / `DeleteMessage` in `server/ai_handler.go`, backed by `database.DeleteMessagesFromSeq`) truncates the conversation at that message's `seq` — it deletes that message and every message + tool call after it, then (for edit) re-runs the turn. There is no branching history; the tail is gone. `Regenerate` is the same truncate-then-rerun on the last assistant turn. +- **AI turns are one-per-conversation:** the `ai.Manager` holds a keyed try-lock (`tryLockConv`/`unlockConv`) acquired by every mutating entry point (Chat, Resume, RegenerateLast, EditAndResend, DeleteMessageFrom). An overlapping turn on the same conversation is rejected — SSE `error` for streaming paths, `ai.ErrConversationBusy` → 409 for the JSON delete. `database.InsertMessage` assigns `seq` atomically inside the INSERT (`MAX(seq)+1` subquery); never split it back into a SELECT-then-INSERT. +- **AI gateway lifecycle:** `ai.Manager.Close()` releases the embedded Bifrost pools and is called from `Server.Shutdown` (via `s.router.ai`). The gateway is built lazily and rebuilt on provider-config change (`invalidateClient`). - **Docs single source:** `docs/reference.md` is the canonical Orva reference markdown (~53 KB). `make docs-embed` syncs it to `backend/internal/mcp/reference.md` (embedded by the `get_orva_docs` MCP tool) and `frontend/public/docs.md` (served at `/docs.md` for the dashboard's Copy as Markdown button). Edit the canonical file then run `make docs-embed`; the Vue Docs page is the rendered version (separate templates) and must be updated alongside if content changes. diff --git a/backend/internal/ai/agent/agent.go b/backend/internal/ai/agent/agent.go new file mode 100644 index 0000000..98635d2 --- /dev/null +++ b/backend/internal/ai/agent/agent.go @@ -0,0 +1,469 @@ +// Package agent runs Orva's in-product AI assistant: the agentic loop that +// streams a model turn, detects tool calls, gates writes behind human +// approval, dispatches approved calls in-process, and feeds results back to +// the model until it produces a final answer. +// +// The loop is deliberately decoupled from the tool layer: it receives a set of +// Tools (name + JSON schema + approval metadata) and a Dispatcher, so it never +// imports the mcp package. ai.Manager wires the mcp registry to it. +package agent + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "strings" + "time" + + "github.com/Harsh-2002/Orva/backend/internal/ai/llm" + "github.com/Harsh-2002/Orva/backend/internal/database" +) + +// Tool is one callable the agent may offer the model, plus the metadata that +// drives approval gating and UI grouping. +type Tool struct { + Def llm.ToolDef + Group string + Perm string // read | write | invoke | admin + ReadOnly bool + Destructive bool +} + +// Dispatcher executes a tool by name with raw JSON arguments and returns the +// tool's output (already JSON-encoded). Implemented over the mcp registry. +type Dispatcher func(ctx context.Context, name string, args json.RawMessage) (json.RawMessage, error) + +// Store is the persistence surface the loop needs. *database.Database +// satisfies it. +type Store interface { + InsertMessage(m *database.AIMessage) error + UpdateMessage(id, content, parts, tokenUsage string) error + ListMessages(conversationID string, sinceSeq int) ([]*database.AIMessage, error) + InsertToolCall(t *database.AIToolCall) error + GetToolCall(id string) (*database.AIToolCall, error) + UpdateToolCall(t *database.AIToolCall) error + ListToolCalls(conversationID string) ([]*database.AIToolCall, error) + TouchConversation(id string) error +} + +// Sink writes SSE frames to the client. The handler implements it over the +// http.ResponseWriter (with flushing). +type Sink interface { + Send(event string, data any) error +} + +// Config is the per-run agent configuration (resolved from ai_settings). +type Config struct { + Provider string + Model string + Thinking string // off | standard | deep + System string // system prompt + Temperature *float64 + ApprovalPolicy string // all_writes | destructive_only | auto + MaxIterations int +} + +// Runner executes agent turns for one configured provider/model. +type Runner struct { + llm *llm.Client + tools []Tool + byName map[string]Tool + dispatch Dispatcher + store Store + cfg Config +} + +// New builds a Runner. tools is the principal-scoped catalog; dispatch runs +// them in-process. +func New(client *llm.Client, tools []Tool, dispatch Dispatcher, store Store, cfg Config) *Runner { + if cfg.MaxIterations <= 0 { + cfg.MaxIterations = 25 + } + byName := make(map[string]Tool, len(tools)) + for _, t := range tools { + byName[t.Def.Name] = t + } + return &Runner{llm: client, tools: tools, byName: byName, dispatch: dispatch, store: store, cfg: cfg} +} + +// ─── message parts (stored in ai_messages.parts; also the frontend's render model) ── + +type part struct { + Type string `json:"type"` // text | thinking | tool_call | tool_result + + Text string `json:"text,omitempty"` + + // tool_call + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Arguments string `json:"arguments,omitempty"` + Group string `json:"group,omitempty"` + + // tool_result + CallID string `json:"call_id,omitempty"` + Status string `json:"status,omitempty"` +} + +// ─── public entry points ──────────────────────────────────────────────────── + +// Run handles a fresh user message: persists it, then advances the agentic +// loop. principalID is recorded as the approver/actor. +func (r *Runner) Run(ctx context.Context, sink Sink, convID, userContent, principalID string) error { + if err := r.store.InsertMessage(&database.AIMessage{ + ConversationID: convID, + Role: "user", + Content: userContent, + Parts: mustJSON([]part{{Type: "text", Text: userContent}}), + }); err != nil { + return err + } + _ = r.store.TouchConversation(convID) + return r.advance(ctx, sink, convID, principalID) +} + +// Resume continues a paused turn after the user approves or rejects a tool +// call. If approved, the tool runs now; once every pending tool call for the +// triggering assistant message is resolved, the loop advances. +func (r *Runner) Resume(ctx context.Context, sink Sink, convID, toolCallID string, approved bool, principalID string) error { + tc, err := r.store.GetToolCall(toolCallID) + if err != nil { + return err + } + if tc.Status != "pending_approval" { + return fmt.Errorf("tool call %s is not awaiting approval (status=%s)", toolCallID, tc.Status) + } + + if !approved { + tc.Status = "rejected" + tc.ApprovedBy = principalID + _ = r.store.UpdateToolCall(tc) + // Record a tool message so the model learns the call was declined. + r.persistToolResult(convID, tc, `{"error":"rejected by user"}`) + _ = sink.Send("tool_result", map[string]any{"id": tc.ID, "status": "rejected"}) + } else { + tc.Status = "approved" + tc.ApprovedBy = principalID + _ = r.store.UpdateToolCall(tc) + r.runToolCall(ctx, sink, convID, tc) + } + + // Are any tool calls for the same assistant message still pending? + if r.hasPendingForMessage(convID, tc.MessageID) { + _ = sink.Send("awaiting_approval", map[string]any{"conversation_id": convID}) + return nil // still waiting on the user for another call + } + return r.advance(ctx, sink, convID, principalID) +} + +// ─── core loop ────────────────────────────────────────────────────────────── + +func (r *Runner) advance(ctx context.Context, sink Sink, convID, principalID string) error { + for iter := 0; iter < r.cfg.MaxIterations; iter++ { + // Bail if the client disconnected (or the request was cancelled). + // Otherwise the loop would keep calling the billed provider and + // dispatching auto-approved tools against a dead connection. + if err := ctx.Err(); err != nil { + return err + } + history, err := r.loadHistory(convID) + if err != nil { + return err + } + + req := llm.Request{ + Provider: r.cfg.Provider, + Model: r.cfg.Model, + System: r.cfg.System, + Messages: history, + Tools: r.toolDefs(), + Thinking: r.cfg.Thinking, + Temperature: r.cfg.Temperature, + } + stream, err := r.llm.Stream(ctx, req) + if err != nil { + _ = sink.Send("error", map[string]any{"message": err.Error()}) + return err + } + + // Persist a placeholder assistant message up-front so we have a stable + // id to attach tool calls to and to update when streaming finishes. + assistant := &database.AIMessage{ConversationID: convID, Role: "assistant"} + if err := r.store.InsertMessage(assistant); err != nil { + return err + } + _ = sink.Send("message_start", map[string]any{"message_id": assistant.ID, "role": "assistant"}) + + var textB, thinkB strings.Builder + var toolCalls []llm.ToolCall + var usage *llm.Usage + for ev := range stream { + switch ev.Type { + case llm.EventText: + textB.WriteString(ev.Text) + _ = sink.Send("delta", map[string]any{"text": ev.Text}) + case llm.EventThinking: + thinkB.WriteString(ev.Text) + _ = sink.Send("thinking", map[string]any{"text": ev.Text}) + case llm.EventDone: + toolCalls = ev.ToolCalls + usage = ev.Usage + case llm.EventError: + r.finalizeAssistant(assistant, textB.String(), thinkB.String(), nil, nil) + _ = sink.Send("error", map[string]any{"message": ev.Err.Error()}) + return ev.Err + } + } + + r.finalizeAssistant(assistant, textB.String(), thinkB.String(), toolCalls, usage) + _ = sink.Send("message_end", map[string]any{"message_id": assistant.ID}) + _ = r.store.TouchConversation(convID) + + // No tool calls → the model produced a final answer. + if len(toolCalls) == 0 { + _ = sink.Send("done", map[string]any{"conversation_id": convID}) + return nil + } + + // Process the requested tool calls. Read-only / auto ones run now; + // anything gated pauses the turn for approval. + paused := r.processToolCalls(ctx, sink, convID, assistant.ID, toolCalls) + if paused { + _ = sink.Send("awaiting_approval", map[string]any{"conversation_id": convID}) + return nil + } + // All tool results recorded; loop and let the model continue. + } + + _ = sink.Send("done", map[string]any{"conversation_id": convID, "note": "max tool iterations reached"}) + return nil +} + +// processToolCalls persists each requested call, emits its event, runs the +// non-gated ones immediately, and returns true if any call is awaiting +// approval (the turn must pause). +func (r *Runner) processToolCalls(ctx context.Context, sink Sink, convID, msgID string, calls []llm.ToolCall) (paused bool) { + for _, c := range calls { + meta, known := r.byName[c.Name] + requiresApproval := known && r.approvalNeeded(meta) + group := "" + destructive := false + if known { + group = meta.Group + destructive = meta.Destructive + } + status := "running" + if requiresApproval { + status = "pending_approval" + } + row := &database.AIToolCall{ + ConversationID: convID, + MessageID: msgID, + CallID: c.ID, + ToolName: c.Name, + ToolGroup: group, + Args: emptyToObj(c.Arguments), + Status: status, + RequiresApproval: requiresApproval, + Destructive: destructive, + } + _ = r.store.InsertToolCall(row) + _ = sink.Send("tool_call", map[string]any{ + "id": row.ID, "call_id": c.ID, "name": c.Name, "group": group, + "args": json.RawMessage(row.Args), "requires_approval": requiresApproval, + }) + + if requiresApproval { + paused = true + continue // wait for the user; do NOT run it + } + r.runToolCall(ctx, sink, convID, row) + } + return paused +} + +// runToolCall dispatches one (approved or auto) tool call, persists the +// result, records a tool message for the model, and emits tool_result. +func (r *Runner) runToolCall(ctx context.Context, sink Sink, convID string, row *database.AIToolCall) { + started := time.Now().UTC() + row.StartedAt = &started + row.Status = "running" + _ = r.store.UpdateToolCall(row) + + out, err := r.dispatch(ctx, row.ToolName, json.RawMessage(emptyToObj(row.Args))) + var resultJSON string + if err != nil { + resultJSON = mustJSON(map[string]string{"error": err.Error()}) + row.Status = "failed" + } else { + resultJSON = string(out) + if resultJSON == "" { + resultJSON = "null" + } + row.Status = "succeeded" + } + finished := time.Now().UTC() + durMS := finished.Sub(started).Milliseconds() + row.FinishedAt = &finished + row.DurationMS = &durMS + row.Result = resultJSON + _ = r.store.UpdateToolCall(row) + r.persistToolResult(convID, row, resultJSON) + _ = sink.Send("tool_result", map[string]any{ + "id": row.ID, "call_id": row.CallID, "status": row.Status, + "result": json.RawMessage(resultJSON), + }) +} + +// ─── persistence + history helpers ─────────────────────────────────────────── + +func (r *Runner) finalizeAssistant(m *database.AIMessage, text, thinking string, calls []llm.ToolCall, usage *llm.Usage) { + parts := make([]part, 0, 2+len(calls)) + if thinking != "" { + parts = append(parts, part{Type: "thinking", Text: thinking}) + } + if text != "" { + parts = append(parts, part{Type: "text", Text: text}) + } + for _, c := range calls { + meta := r.byName[c.Name] + parts = append(parts, part{Type: "tool_call", ID: c.ID, Name: c.Name, Arguments: c.Arguments, Group: meta.Group}) + } + usageJSON := "" + if usage != nil { + usageJSON = mustJSON(usage) + } + if err := r.store.UpdateMessage(m.ID, text, mustJSON(parts), usageJSON); err != nil { + // If we can't persist the assistant turn, the next loadHistory will be + // missing it and the model loses context — surface it rather than hide it. + slog.Warn("ai: persist assistant message failed", "conversation", m.ConversationID, "message", m.ID, "error", err) + } +} + +// persistToolResult writes a role=tool message so the model sees the result on +// the next turn (the ai_tool_calls row is the UI/audit view). +func (r *Runner) persistToolResult(convID string, row *database.AIToolCall, resultJSON string) { + _ = r.store.InsertMessage(&database.AIMessage{ + ConversationID: convID, + Role: "tool", + Content: resultJSON, + Parts: mustJSON([]part{{Type: "tool_result", CallID: row.CallID, Status: row.Status}}), + }) +} + +// loadHistory rebuilds the model-facing message list from stored messages. +func (r *Runner) loadHistory(convID string) ([]llm.Message, error) { + rows, err := r.store.ListMessages(convID, 0) + if err != nil { + return nil, err + } + out := make([]llm.Message, 0, len(rows)) + for _, m := range rows { + switch m.Role { + case "user": + out = append(out, llm.Message{Role: llm.RoleUser, Content: m.Content}) + case "assistant": + msg := llm.Message{Role: llm.RoleAssistant, Content: textFromParts(m.Parts)} + for _, p := range parseParts(m.Parts) { + if p.Type == "tool_call" { + msg.ToolCalls = append(msg.ToolCalls, llm.ToolCall{ID: p.ID, Name: p.Name, Arguments: p.Arguments}) + } + } + // Skip empty assistant messages (no text, no tool calls) to avoid + // confusing the provider with blank turns. + if msg.Content == "" && len(msg.ToolCalls) == 0 { + continue + } + out = append(out, msg) + case "tool": + callID := "" + for _, p := range parseParts(m.Parts) { + if p.Type == "tool_result" { + callID = p.CallID + } + } + out = append(out, llm.Message{Role: llm.RoleTool, Content: m.Content, ToolCallID: callID}) + } + } + return out, nil +} + +// hasPendingForMessage reports whether any tool call belonging to the given +// assistant message is still awaiting approval. Used to decide, after one +// approve/reject, whether the turn can advance or must keep waiting (an +// assistant turn may request several gated tools at once). +func (r *Runner) hasPendingForMessage(convID, msgID string) bool { + rows, err := r.store.ListToolCalls(convID) + if err != nil { + // On a read failure, assume still-pending and keep waiting rather than + // advancing the turn (which could run past an unresolved approval gate). + slog.Warn("ai: list tool calls failed; treating turn as still pending", "conversation", convID, "error", err) + return true + } + for _, tc := range rows { + if tc.MessageID == msgID && tc.Status == "pending_approval" { + return true + } + } + return false +} + +// ─── policy + small helpers ────────────────────────────────────────────────── + +// approvalNeeded applies the configured policy. Reads/invoke never need +// approval; writes/admin depend on policy. +func (r *Runner) approvalNeeded(t Tool) bool { + if t.ReadOnly || t.Perm == "read" || t.Perm == "invoke" { + return false + } + switch r.cfg.ApprovalPolicy { + case "auto": + return false + case "destructive_only": + return t.Destructive + default: // all_writes + return true + } +} + +func (r *Runner) toolDefs() []llm.ToolDef { + out := make([]llm.ToolDef, 0, len(r.tools)) + for _, t := range r.tools { + out = append(out, t.Def) + } + return out +} + +func parseParts(s string) []part { + if s == "" { + return nil + } + var ps []part + _ = json.Unmarshal([]byte(s), &ps) + return ps +} + +func textFromParts(s string) string { + var b strings.Builder + for _, p := range parseParts(s) { + if p.Type == "text" { + b.WriteString(p.Text) + } + } + return b.String() +} + +func mustJSON(v any) string { + b, err := json.Marshal(v) + if err != nil { + return "null" + } + return string(b) +} + +func emptyToObj(s string) string { + if strings.TrimSpace(s) == "" { + return "{}" + } + return s +} diff --git a/backend/internal/ai/llm/account.go b/backend/internal/ai/llm/account.go new file mode 100644 index 0000000..4b791d2 --- /dev/null +++ b/backend/internal/ai/llm/account.go @@ -0,0 +1,56 @@ +package llm + +import ( + "context" + "fmt" + "strings" + + "github.com/maximhq/bifrost/core/schemas" +) + +// KeyResolver supplies provider credentials to the gateway on demand. It is +// implemented over Orva's ai_provider_configs table (keys decrypted via the +// secrets cipher). Lookups are live, so a freshly-added key is picked up the +// next time the gateway resolves it — no restart. +type KeyResolver interface { + // Providers returns the providers that currently have an enabled key. + Providers() []string + // Resolve returns the decrypted API key and optional base-URL override for + // a provider, or an error if none is configured. + Resolve(provider string) (apiKey, baseURL string, err error) +} + +// account adapts a KeyResolver to Bifrost's schemas.Account interface. +type account struct{ resolver KeyResolver } + +func (a *account) GetConfiguredProviders() ([]schemas.ModelProvider, error) { + names := a.resolver.Providers() + out := make([]schemas.ModelProvider, 0, len(names)) + for _, n := range names { + out = append(out, schemas.ModelProvider(strings.ToLower(n))) + } + return out, nil +} + +func (a *account) GetKeysForProvider(_ context.Context, providerKey schemas.ModelProvider) ([]schemas.Key, error) { + apiKey, _, err := a.resolver.Resolve(string(providerKey)) + if err != nil { + return nil, fmt.Errorf("no key configured for provider %q: %w", providerKey, err) + } + return []schemas.Key{{ + ID: string(providerKey), + Value: schemas.EnvVar{Val: apiKey}, + Models: schemas.WhiteList{"*"}, // this key may serve any model (empty list = none allowed) + Weight: 1.0, + }}, nil +} + +func (a *account) GetConfigForProvider(providerKey schemas.ModelProvider) (*schemas.ProviderConfig, error) { + cfg := &schemas.ProviderConfig{ + ConcurrencyAndBufferSize: schemas.ConcurrencyAndBufferSize{Concurrency: 3, BufferSize: 10}, + } + if _, baseURL, err := a.resolver.Resolve(string(providerKey)); err == nil && baseURL != "" { + cfg.NetworkConfig.BaseURL = baseURL + } + return cfg, nil +} diff --git a/backend/internal/ai/llm/llm.go b/backend/internal/ai/llm/llm.go new file mode 100644 index 0000000..da6f0ad --- /dev/null +++ b/backend/internal/ai/llm/llm.go @@ -0,0 +1,263 @@ +package llm + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + + bifrost "github.com/maximhq/bifrost/core" + "github.com/maximhq/bifrost/core/schemas" +) + +// Client is an embedded Bifrost gateway scoped to one KeyResolver. Construct +// it once (it owns provider connection pools); rebuild it when the set of +// configured providers changes (see ai.Manager). +type Client struct { + bf *bifrost.Bifrost +} + +// New initialises the in-process gateway. The resolver supplies credentials +// lazily, so New succeeds even with zero providers configured; requests for an +// unconfigured provider fail at call time with a clear error. +func New(resolver KeyResolver) (*Client, error) { + bf, err := bifrost.Init(context.Background(), schemas.BifrostConfig{ + Account: &account{resolver: resolver}, + Logger: bifrost.NewNoOpLogger(), + InitialPoolSize: 10, + }) + if err != nil { + return nil, fmt.Errorf("init bifrost: %w", err) + } + return &Client{bf: bf}, nil +} + +// Close releases the gateway's pools. +func (c *Client) Close() { + if c.bf != nil { + c.bf.Shutdown() + } +} + +// Stream runs one streaming chat completion and returns a channel of neutral +// events. The channel closes after EventDone or EventError. The request's +// context cancels the underlying provider call (e.g. on client disconnect). +func (c *Client) Stream(ctx context.Context, req Request) (<-chan Event, error) { + breq, err := buildRequest(req) + if err != nil { + return nil, err + } + + bctx, cancel := schemas.NewBifrostContextWithCancel(ctx) + stream, berr := c.bf.ChatCompletionStreamRequest(bctx, breq) + if berr != nil { + cancel() + return nil, errors.New(bifrostErr(berr)) + } + + out := make(chan Event, 32) + go func() { + defer close(out) + defer cancel() + + // Tool calls arrive incrementally (per OpenAI streaming): each delta + // carries a tool-call index plus a fragment of the name/arguments. We + // accumulate by index and assemble the complete calls at finish. + type acc struct { + id, name string + args strings.Builder + } + byIndex := map[int]*acc{} + var order []int + var finish string + var usage *Usage + + for chunk := range stream { + if chunk == nil { + continue + } + if chunk.BifrostError != nil { + out <- Event{Type: EventError, Err: errors.New(bifrostErr(chunk.BifrostError))} + return + } + resp := chunk.BifrostChatResponse + if resp == nil { + continue + } + if resp.Usage != nil { + usage = &Usage{ + PromptTokens: resp.Usage.PromptTokens, + CompletionTokens: resp.Usage.CompletionTokens, + TotalTokens: resp.Usage.TotalTokens, + } + } + for i := range resp.Choices { + choice := resp.Choices[i] + if choice.FinishReason != nil && *choice.FinishReason != "" { + finish = *choice.FinishReason + } + sc := choice.ChatStreamResponseChoice + if sc == nil || sc.Delta == nil { + continue + } + d := sc.Delta + if d.Content != nil && *d.Content != "" { + out <- Event{Type: EventText, Text: *d.Content} + } + if d.Reasoning != nil && *d.Reasoning != "" { + out <- Event{Type: EventThinking, Text: *d.Reasoning} + } + for _, tc := range d.ToolCalls { + idx := int(tc.Index) + a := byIndex[idx] + if a == nil { + a = &acc{} + byIndex[idx] = a + order = append(order, idx) + } + if tc.ID != nil && *tc.ID != "" { + a.id = *tc.ID + } + if tc.Function.Name != nil && *tc.Function.Name != "" { + a.name = *tc.Function.Name + } + a.args.WriteString(tc.Function.Arguments) + } + } + } + + calls := make([]ToolCall, 0, len(order)) + for _, idx := range order { + a := byIndex[idx] + if a.name == "" { + continue + } + calls = append(calls, ToolCall{ID: a.id, Name: a.name, Arguments: a.args.String()}) + } + out <- Event{Type: EventDone, ToolCalls: calls, FinishReason: finish, Usage: usage} + }() + + return out, nil +} + +// buildRequest translates a neutral Request into a Bifrost chat request. +func buildRequest(req Request) (*schemas.BifrostChatRequest, error) { + if strings.TrimSpace(req.Provider) == "" || strings.TrimSpace(req.Model) == "" { + return nil, errors.New("provider and model are required") + } + + input := make([]schemas.ChatMessage, 0, len(req.Messages)+1) + if s := strings.TrimSpace(req.System); s != "" { + input = append(input, textMessage(schemas.ChatMessageRoleSystem, req.System)) + } + for _, m := range req.Messages { + input = append(input, toBifrostMessage(m)) + } + + params := &schemas.ChatParameters{} + if req.Temperature != nil { + params.Temperature = req.Temperature + } + // Cache the (large, stable) system prompt so providers that support prompt + // caching charge a fraction for it after the first call. Bifrost applies + // this only to provider families that support it (Anthropic); OpenAI-family + // providers cache automatically and ignore the field, so this is safe for + // custom OpenAI-compatible endpoints too. + if strings.TrimSpace(req.System) != "" { + params.CacheControl = &schemas.CacheControl{Type: schemas.CacheControlType("ephemeral")} + } + if r := reasoningFor(req.Thinking); r != nil { + params.Reasoning = r + } + if len(req.Tools) > 0 { + tools, err := toBifrostTools(req.Tools) + if err != nil { + return nil, err + } + params.Tools = tools + } + + return &schemas.BifrostChatRequest{ + Provider: schemas.ModelProvider(strings.ToLower(req.Provider)), + Model: req.Model, + Input: input, + Params: params, + }, nil +} + +func textMessage(role schemas.ChatMessageRole, text string) schemas.ChatMessage { + t := text + return schemas.ChatMessage{Role: role, Content: &schemas.ChatMessageContent{ContentStr: &t}} +} + +func toBifrostMessage(m Message) schemas.ChatMessage { + switch m.Role { + case RoleAssistant: + msg := schemas.ChatMessage{Role: schemas.ChatMessageRoleAssistant} + if m.Content != "" { + c := m.Content + msg.Content = &schemas.ChatMessageContent{ContentStr: &c} + } + if len(m.ToolCalls) > 0 { + calls := make([]schemas.ChatAssistantMessageToolCall, 0, len(m.ToolCalls)) + for _, tc := range m.ToolCalls { + id := tc.ID + name := tc.Name + calls = append(calls, schemas.ChatAssistantMessageToolCall{ + Type: ptrStr("function"), + ID: &id, + Function: schemas.ChatAssistantMessageToolCallFunction{ + Name: &name, + Arguments: tc.Arguments, + }, + }) + } + msg.ChatAssistantMessage = &schemas.ChatAssistantMessage{ToolCalls: calls} + } + return msg + case RoleTool: + c := m.Content + id := m.ToolCallID + return schemas.ChatMessage{ + Role: schemas.ChatMessageRoleTool, + Content: &schemas.ChatMessageContent{ContentStr: &c}, + ChatToolMessage: &schemas.ChatToolMessage{ToolCallID: &id}, + } + case RoleSystem: + return textMessage(schemas.ChatMessageRoleSystem, m.Content) + default: + return textMessage(schemas.ChatMessageRoleUser, m.Content) + } +} + +func toBifrostTools(defs []ToolDef) ([]schemas.ChatTool, error) { + out := make([]schemas.ChatTool, 0, len(defs)) + for _, d := range defs { + fn := &schemas.ChatToolFunction{Name: d.Name} + if d.Description != "" { + desc := d.Description + fn.Description = &desc + } + if len(d.Schema) > 0 { + var params schemas.ToolFunctionParameters + if err := json.Unmarshal(d.Schema, ¶ms); err != nil { + return nil, fmt.Errorf("tool %s: bad schema: %w", d.Name, err) + } + fn.Parameters = ¶ms + } + out = append(out, schemas.ChatTool{Type: schemas.ChatToolTypeFunction, Function: fn}) + } + return out, nil +} + +// bifrostErr renders a Bifrost error to a readable string. +func bifrostErr(e *schemas.BifrostError) string { + if e == nil { + return "unknown bifrost error" + } + if s := e.GetErrorString(); s != "" { + return s + } + return e.String() +} diff --git a/backend/internal/ai/llm/thinking.go b/backend/internal/ai/llm/thinking.go new file mode 100644 index 0000000..916a10a --- /dev/null +++ b/backend/internal/ai/llm/thinking.go @@ -0,0 +1,33 @@ +package llm + +import "github.com/maximhq/bifrost/core/schemas" + +// reasoningFor maps Orva's three UI thinking levels onto Bifrost's neutral +// reasoning config. Bifrost normalises this per provider: OpenAI/Bedrock use +// the effort string, Anthropic/Gemini/Cohere use the token budget. We set both +// so whichever strategy the chosen provider uses is satisfied. Anthropic +// requires a budget ≥1024, so the smallest budget we ever send is 4096. +// +// Returns nil for "off" (and any unknown value), which disables reasoning. +func reasoningFor(level string) *schemas.ChatReasoning { + switch level { + case "standard": + return &schemas.ChatReasoning{ + Enabled: ptrBool(true), + Effort: ptrStr("medium"), + MaxTokens: ptrInt(4096), + } + case "deep": + return &schemas.ChatReasoning{ + Enabled: ptrBool(true), + Effort: ptrStr("high"), + MaxTokens: ptrInt(16384), + } + default: // "off" or unrecognised + return nil + } +} + +func ptrStr(s string) *string { return &s } +func ptrBool(b bool) *bool { return &b } +func ptrInt(i int) *int { return &i } diff --git a/backend/internal/ai/llm/types.go b/backend/internal/ai/llm/types.go new file mode 100644 index 0000000..fad9249 --- /dev/null +++ b/backend/internal/ai/llm/types.go @@ -0,0 +1,93 @@ +// Package llm wraps the embedded Bifrost AI gateway (github.com/maximhq/bifrost) +// behind a small, provider-neutral interface. The agent loop works only with +// the types here — Message, ToolDef, Request, Event — and never sees a Bifrost +// schema type, so the gateway stays swappable and the loop stays testable. +package llm + +import "encoding/json" + +// Role is a chat message role. +type Role string + +const ( + RoleSystem Role = "system" + RoleUser Role = "user" + RoleAssistant Role = "assistant" + RoleTool Role = "tool" +) + +// ToolCall is one tool invocation the model requested (or that we replay back +// to the model on the next turn). Arguments is the raw JSON string the model +// produced — it may need repair, so it is kept as a string until dispatch. +type ToolCall struct { + ID string `json:"id"` + Name string `json:"name"` + Arguments string `json:"arguments"` +} + +// Message is one entry in the conversation sent to the model. +// - user/system: Content only. +// - assistant: Content and/or ToolCalls. +// - tool: Content (the result) + ToolCallID it answers. +type Message struct { + Role Role + Content string + ToolCalls []ToolCall // assistant only + ToolCallID string // tool only +} + +// ToolDef is a tool the model may call. Schema is a JSON Schema object for the +// arguments (produced by the agent tool registry). +type ToolDef struct { + Name string + Description string + Schema json.RawMessage +} + +// Request is one streaming chat completion. +type Request struct { + Provider string // openai | anthropic | bedrock | gemini | groq | ollama | … + Model string + System string + Messages []Message + Tools []ToolDef + Thinking string // off | standard | deep + Temperature *float64 // nil = provider default +} + +// Usage is token accounting for one model turn. +type Usage struct { + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + ReasoningTokens int `json:"reasoning_tokens,omitempty"` + TotalTokens int `json:"total_tokens"` +} + +// EventType discriminates the streamed events from Stream. +type EventType string + +const ( + EventText EventType = "text" // a text token delta + EventThinking EventType = "thinking" // a reasoning token delta + EventDone EventType = "done" // model turn finished (carries assembled tool calls + usage) + EventError EventType = "error" // fatal error; stream ends after this +) + +// Event is one item on the stream returned by Client.Stream. +type Event struct { + Type EventType + + // Text is set for EventText and EventThinking (the incremental delta). + Text string + + // ToolCalls is set on EventDone: the fully-assembled tool calls the model + // requested this turn (empty if the model produced only text). + ToolCalls []ToolCall + + // FinishReason and Usage are set on EventDone when the provider reports them. + FinishReason string + Usage *Usage + + // Err is set on EventError. + Err error +} diff --git a/backend/internal/ai/manager.go b/backend/internal/ai/manager.go new file mode 100644 index 0000000..0e30260 --- /dev/null +++ b/backend/internal/ai/manager.go @@ -0,0 +1,807 @@ +// Package ai is the service layer for Orva's in-product AI chat assistant. It +// wires together: the SQLite store (database), the at-rest key cipher +// (secrets), the in-process tool registry (mcp.BuildAgentRegistry), the +// embedded LLM gateway (llm, Bifrost), and the agentic loop (agent). Handlers +// call Manager; Manager owns all the policy. +package ai + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "net/http" + "strings" + "sync" + "time" + + "github.com/Harsh-2002/Orva/backend/internal/ai/agent" + "github.com/Harsh-2002/Orva/backend/internal/ai/llm" + "github.com/Harsh-2002/Orva/backend/internal/auth" + "github.com/Harsh-2002/Orva/backend/internal/database" + "github.com/Harsh-2002/Orva/backend/internal/mcp" + "github.com/Harsh-2002/Orva/backend/internal/secrets" +) + +// Sink is the SSE event sink the handler implements over the HTTP response. +type Sink = agent.Sink + +// Principal identifies the chat caller and scopes which tools the agent may +// use. Perms mirrors the MCP/REST permission model. +type Principal struct { + ID string + Label string + Perms auth.PermSet +} + +// Manager is the long-lived AI service, constructed once in server.New(). +type Manager struct { + db *database.Database + secrets *secrets.Manager + deps mcp.Deps // tool registry dependencies (same struct the MCP server uses) + + mu sync.Mutex + client *llm.Client + dirty bool // rebuild the gateway on next use (provider configs changed) + + // convMu guards convBusy, the set of conversations with a turn in flight. + // One turn per conversation: overlapping turns (double-send, or chat while + // an approval is being decided) are rejected rather than interleaved, which + // would corrupt message ordering. + convMu sync.Mutex + convBusy map[string]bool +} + +// ErrConversationBusy is returned when a turn is already running for a +// conversation. Streaming entry points surface it as an SSE error; the JSON +// delete path maps it to 409 Conflict. +var ErrConversationBusy = errors.New("a turn is already in progress for this conversation") + +// tryLockConv marks a conversation busy, returning false if it already is. +func (m *Manager) tryLockConv(id string) bool { + m.convMu.Lock() + defer m.convMu.Unlock() + if m.convBusy[id] { + return false + } + if m.convBusy == nil { + m.convBusy = map[string]bool{} + } + m.convBusy[id] = true + return true +} + +func (m *Manager) unlockConv(id string) { + m.convMu.Lock() + delete(m.convBusy, id) + m.convMu.Unlock() +} + +// New constructs the Manager. The LLM gateway is built lazily on first chat +// (and rebuilt when provider configs change), so startup never depends on a +// provider being configured. +func New(db *database.Database, sec *secrets.Manager, deps mcp.Deps) *Manager { + return &Manager{db: db, secrets: sec, deps: deps, dirty: true} +} + +// Close releases the LLM gateway pools. +func (m *Manager) Close() { + m.mu.Lock() + defer m.mu.Unlock() + if m.client != nil { + m.client.Close() + m.client = nil + } +} + +// ─── llm.KeyResolver (credentials come from ai_provider_configs, decrypted) ── + +// Providers lists providers that currently have an enabled key. +func (m *Manager) Providers() []string { + cfgs, err := m.db.ListProviderConfigs() + if err != nil { + return nil + } + seen := map[string]bool{} + var out []string + for _, c := range cfgs { + if c.Enabled && c.APIKeyEncrypted != "" && !seen[c.Provider] { + seen[c.Provider] = true + out = append(out, c.Provider) + } + } + return out +} + +// Resolve returns the decrypted API key and optional base-URL for a provider. +func (m *Manager) Resolve(provider string) (apiKey, baseURL string, err error) { + cfg, err := m.db.GetEnabledProviderConfig(strings.ToLower(provider)) + if err != nil { + return "", "", fmt.Errorf("provider %q not configured", provider) + } + if cfg.APIKeyEncrypted == "" { + // Local providers (e.g. Ollama) may legitimately have no key. + return "", normalizeBaseURL(cfg.BaseURL), nil + } + key, err := m.secrets.DecryptValue(cfg.APIKeyEncrypted) + if err != nil { + return "", "", fmt.Errorf("decrypt key for %q: %w", provider, err) + } + return key, normalizeBaseURL(cfg.BaseURL), nil +} + +func (m *Manager) getClient() (*llm.Client, error) { + m.mu.Lock() + defer m.mu.Unlock() + if m.client != nil && !m.dirty { + return m.client, nil + } + if m.client != nil { + m.client.Close() + m.client = nil + } + c, err := llm.New(m) + if err != nil { + return nil, err + } + m.client = c + m.dirty = false + return c, nil +} + +func (m *Manager) invalidateClient() { + m.mu.Lock() + m.dirty = true + m.mu.Unlock() +} + +// ─── chat ──────────────────────────────────────────────────────────────────── + +// ChatParams is one inbound chat message plus optional per-message overrides. +type ChatParams struct { + ConversationID string + Content string + Provider string + Model string + Thinking string +} + +// Chat runs one user turn through the agentic loop, streaming events to sink. +// It creates the conversation if ConversationID is empty and returns the +// conversation id used. +func (m *Manager) Chat(ctx context.Context, sink Sink, p Principal, params ChatParams) (string, error) { + settings := m.resolvedSettings() + cfg := agent.Config{ + Provider: firstNonEmpty(params.Provider, settings.Provider), + Model: firstNonEmpty(params.Model, settings.Model), + Thinking: firstNonEmpty(params.Thinking, settings.ThinkingLevel), + System: settings.SystemPrompt, + ApprovalPolicy: settings.ApprovalPolicy, + MaxIterations: settings.MaxToolIterations, + } + + convID := params.ConversationID + if convID == "" { + c := &database.AIConversation{ + UserID: p.ID, + Title: titleFrom(params.Content), + Provider: cfg.Provider, + Model: cfg.Model, + } + if err := m.db.CreateConversation(c); err != nil { + return "", err + } + convID = c.ID + _ = sink.Send("conversation", map[string]any{"id": c.ID, "title": c.Title}) + } + + if !m.tryLockConv(convID) { + _ = sink.Send("error", map[string]any{"message": ErrConversationBusy.Error()}) + return convID, ErrConversationBusy + } + defer m.unlockConv(convID) + + runner, err := m.buildRunner(p.Perms, cfg) + if err != nil { + _ = sink.Send("error", map[string]any{"message": err.Error()}) + return convID, err + } + return convID, runner.Run(ctx, sink, convID, params.Content, p.ID) +} + +// Resume continues a paused turn after an approve/reject decision. +func (m *Manager) Resume(ctx context.Context, sink Sink, p Principal, convID, toolCallID string, approved bool) error { + if !m.tryLockConv(convID) { + _ = sink.Send("error", map[string]any{"message": ErrConversationBusy.Error()}) + return ErrConversationBusy + } + defer m.unlockConv(convID) + settings := m.resolvedSettings() + cfg := agent.Config{ + Provider: settings.Provider, + Model: settings.Model, + Thinking: settings.ThinkingLevel, + System: settings.SystemPrompt, + ApprovalPolicy: settings.ApprovalPolicy, + MaxIterations: settings.MaxToolIterations, + } + // Prefer the conversation's snapshotted provider/model so a resume uses the + // same model that started the turn. + if c, err := m.db.GetConversation(convID); err == nil { + if c.Provider != "" { + cfg.Provider = c.Provider + } + if c.Model != "" { + cfg.Model = c.Model + } + } + runner, err := m.buildRunner(p.Perms, cfg) + if err != nil { + _ = sink.Send("error", map[string]any{"message": err.Error()}) + return err + } + return runner.Resume(ctx, sink, convID, toolCallID, approved, p.ID) +} + +// ChatOverrides are optional per-request provider/model/thinking overrides, +// shared by regenerate and edit-and-resend so a re-run honours the model the +// user currently has selected. +type ChatOverrides struct { + Provider string + Model string + Thinking string +} + +// runTurn runs one turn over a conversation's EXISTING history plus `content` +// (which runner.Run appends as the user message), streaming to sink. Callers +// truncate the conversation first. +func (m *Manager) runTurn(ctx context.Context, sink Sink, p Principal, convID, content string, ov ChatOverrides) error { + settings := m.resolvedSettings() + cfg := agent.Config{ + Provider: firstNonEmpty(ov.Provider, settings.Provider), + Model: firstNonEmpty(ov.Model, settings.Model), + Thinking: firstNonEmpty(ov.Thinking, settings.ThinkingLevel), + System: settings.SystemPrompt, + ApprovalPolicy: settings.ApprovalPolicy, + MaxIterations: settings.MaxToolIterations, + } + runner, err := m.buildRunner(p.Perms, cfg) + if err != nil { + _ = sink.Send("error", map[string]any{"message": err.Error()}) + return err + } + return runner.Run(ctx, sink, convID, content, p.ID) +} + +// RegenerateLast drops the last assistant turn and re-runs the last user message +// for a fresh answer. +func (m *Manager) RegenerateLast(ctx context.Context, sink Sink, p Principal, convID string, ov ChatOverrides) error { + if !m.tryLockConv(convID) { + _ = sink.Send("error", map[string]any{"message": ErrConversationBusy.Error()}) + return ErrConversationBusy + } + defer m.unlockConv(convID) + msgs, err := m.db.ListMessages(convID, 0) + if err != nil { + _ = sink.Send("error", map[string]any{"message": err.Error()}) + return err + } + var last *database.AIMessage + for i := len(msgs) - 1; i >= 0; i-- { + if msgs[i].Role == "user" { + last = msgs[i] + break + } + } + if last == nil { + err := fmt.Errorf("nothing to regenerate") + _ = sink.Send("error", map[string]any{"message": err.Error()}) + return err + } + if err := m.db.DeleteMessagesFromSeq(convID, last.Seq); err != nil { + _ = sink.Send("error", map[string]any{"message": err.Error()}) + return err + } + return m.runTurn(ctx, sink, p, convID, last.Content, ov) +} + +// EditAndResend rewrites a user message, drops it and everything after, and +// re-runs the turn with the new content. +func (m *Manager) EditAndResend(ctx context.Context, sink Sink, p Principal, convID, messageID, content string, ov ChatOverrides) error { + if !m.tryLockConv(convID) { + _ = sink.Send("error", map[string]any{"message": ErrConversationBusy.Error()}) + return ErrConversationBusy + } + defer m.unlockConv(convID) + msg, err := m.db.GetMessage(messageID) + if err != nil || msg.ConversationID != convID { + e := fmt.Errorf("message not found") + _ = sink.Send("error", map[string]any{"message": e.Error()}) + return e + } + if msg.Role != "user" { + e := fmt.Errorf("only user messages can be edited") + _ = sink.Send("error", map[string]any{"message": e.Error()}) + return e + } + if err := m.db.DeleteMessagesFromSeq(convID, msg.Seq); err != nil { + _ = sink.Send("error", map[string]any{"message": err.Error()}) + return err + } + return m.runTurn(ctx, sink, p, convID, content, ov) +} + +// DeleteMessageFrom truncates a conversation from a message onward (that message +// and everything after it), keeping the remaining history coherent. No re-run. +func (m *Manager) DeleteMessageFrom(convID, messageID string) error { + if !m.tryLockConv(convID) { + return ErrConversationBusy + } + defer m.unlockConv(convID) + msg, err := m.db.GetMessage(messageID) + if err != nil || msg.ConversationID != convID { + return fmt.Errorf("message not found") + } + return m.db.DeleteMessagesFromSeq(convID, msg.Seq) +} + +// buildRunner assembles a principal-scoped agent runner: the tool registry is +// gated to the caller's perms, the gateway is the shared client, and dispatch +// runs tools in-process. +func (m *Manager) buildRunner(perms auth.PermSet, cfg agent.Config) (*agent.Runner, error) { + client, err := m.getClient() + if err != nil { + return nil, fmt.Errorf("ai gateway unavailable: %w", err) + } + reg := mcp.BuildAgentRegistry(m.deps, perms) + tools := make([]agent.Tool, 0) + for _, t := range reg.Tools() { + tools = append(tools, agent.Tool{ + Def: llm.ToolDef{Name: t.Name, Description: t.Description, Schema: t.Schema}, + Group: t.Group, + Perm: t.Perm, + ReadOnly: t.ReadOnly, + Destructive: t.Destructive, + }) + } + dispatch := func(ctx context.Context, name string, args json.RawMessage) (json.RawMessage, error) { + out, err := reg.Dispatch(ctx, name, args) + if err != nil { + return nil, err + } + b, mErr := json.Marshal(out) + if mErr != nil { + return nil, mErr + } + return b, nil + } + return agent.New(client, tools, dispatch, m.db, cfg), nil +} + +// ─── settings ──────────────────────────────────────────────────────────────── + +// defaultSystemPrompt is a structured, token-efficient operator-agent prompt. +// Design choices (prompt-engineering best practice): +// - Role + operating rules are explicit and concise (high signal, low tokens). +// - The ~70 KB full reference is NOT inlined every request (that would blow up +// cost and context); instead a condensed ESSENTIALS section covers the +// high-frequency facts, and the agent is told to call get_orva_docs for the +// complete, always-current reference on any detail. The docs the tool +// returns are the single-source docs/reference.md, embedded at build time — +// so they are always up to date with the running build. +// - The whole prompt is stable across requests, so providers with prompt +// caching (Anthropic/OpenAI) cache it and charge a fraction after the first +// call (see cache hints in internal/ai/llm). +const defaultSystemPrompt = `# Role +You are Orva's built-in operator assistant, embedded in the Orva dashboard. Orva +is a self-hosted serverless platform (Functions-as-a-Service): users write +JavaScript/TypeScript (Node 22/24) or Python (3.13/3.14) handlers; Orva builds +them and runs them in nsjail sandboxes, exposed over HTTP. You operate THIS +instance on the user's behalf through tools. + +# How to work +- Act, don't narrate. Prefer doing the task with tools over describing how the + user could do it. Take the next concrete step. +- Inspect before you mutate. Read current state (list_*/get_*) before changing it. +- Don't guess Orva's APIs, handler contract, SDK, config, or limits. When you + need authoritative detail beyond the essentials below, call get_orva_docs — it + returns the complete, current Orva reference. Avoid re-fetching the same topic + in a turn; consulting it again for a different area is fine. +- Writes are gated by the approval policy (default all_writes). Reads and invokes + never pause. Under all_writes every write pauses for operator approval; + destructive_only pauses only destructive-marked tools; auto runs everything. + Just request the write — the platform enforces the gate. A gated call pauses + the turn (it shows as awaiting approval) and resumes with the result once + approved; never assume a write ran until you see its result. +- Destructive tools (delete_*, rollback_function, bulk_delete_executions, + kv_delete, system_vacuum) need an explicit confirm=true argument — a separate + guard the tool enforces in code even when approval is auto. Set confirm=true + only when the user clearly asked for that destructive action, and never retry a + failed destructive call without fresh user intent. +- Close the loop on failures: if an invocation or build fails, read the full + build/execution log + stderr, diagnose ALL issues at once (syntax, deps, + logic), fix them together, and redeploy once — don't redeploy after each single + fix, and don't stop at the first error. +- Spend steps wisely. Each turn has a bounded tool budget (about 25 calls). Batch + work (deploy then invoke once; fix everything from one log) rather than + one-tool-per-item loops. If you run low, finish the highest-value step and tell + the user what remains. + +# Handling queries +- "How do I…/what does Orva support…" → answer from the essentials; for anything + deeper or version-specific, get_orva_docs first, then answer precisely. +- "Build/deploy/fix X" → create_function → deploy_function_inline (the handler + source, wait=true) → invoke_function to verify → on failure read the full log, + fix all issues together, redeploy once. +- "Why is X failing/slow" → list_executions / get_execution_logs / list_traces / + get_function_baseline, then explain and offer a fix. +- Only ask the user a question when a choice genuinely can't be inferred. + +# Orva essentials (quick reference; use get_orva_docs for the full spec) +- Runtimes: node22, node24, python313, python314 (TypeScript runs on the node + runtime). Entrypoint defaults: handler.js (JS/TS) or handler.py (Python). +- Handler contract: the handler receives an event (method, path, headers, body, + query) and returns a response (statusCode, headers, body). Exact shape per + runtime is in get_orva_docs. +- Ship a function: create_function (name, runtime, limits) → deploy_function_inline + (code [+ dependencies = package.json/requirements.txt], wait=true) → + invoke_function (method REQUIRED) → get_execution_logs on failure. +- Networking: network_mode is "none" (loopback only — the default) or "egress" + (outbound HTTPS + the in-sandbox SDK). Set egress at create time if the handler + uses the orva SDK (kv/invoke/jobs) or any external HTTPS. Flipping it drains the + warm pool, so the next invoke is a (normal) cold start. ENETUNREACH/ECONNREFUSED + on an SDK/HTTP handler means it's still on "none" — switch to egress and retry. +- In-sandbox SDK (egress): per-function KV (orva.kv), function-to-function invoke + (orva.invoke), and background jobs (orva.jobs). +- set_secret/delete_secret are idempotent but drain the function's warm pool (next + invoke is a cold start); don't loop secret writes in production. +- Also available: secrets (encrypted env injection), custom routes, cron + schedules, background jobs with retries, system-event webhooks, signed inbound + webhooks, saved request fixtures, egress firewall rules, causal traces, and + system health/metrics/storage. One tool per capability. + +# Output +- First line = the user-facing outcome (for example: Deployed hello v5; invoke + returned 200). Then optional brief detail or next steps. Keep prose tight. +- Wrap tool names, ids, config keys, and short values in inline code. Use fenced + code blocks, always with a language label (python, bash, json), for handlers, + command output, or JSON. Tool outputs (logs, JSON) auto-render in collapsible + cards and large code auto-collapses, so don't re-print raw tool output or + apologize for length. Default to prose and bullet lists — including for + multi-attribute entity listings (functions, jobs, executions, secrets, cron): + give each item ONE bullet with its details inline (e.g. "hello: node24, active, + egress"). Do NOT render a routine listing as a Markdown table. Use a table ONLY + when the user explicitly asks to compare items side by side. +- Surface execution/trace/job ids in prose only when the user is debugging a + specific run; otherwise they already show in tool cards — keep summaries clean. +- Never surface secret/key/token values — not even prefixes or hashes. The secret + and key tools never return plaintext; the only way a secret reaches you is if + user code logged it. If a value appears in logs, stderr, or KV, redact it and + describe the symptom (for example: invalid auth header), not the value.` + +// resolvedSettings returns the stored settings with built-in defaults filled +// in for any missing field. +func (m *Manager) resolvedSettings() database.AISettings { + s, ok, err := m.db.GetSettings("default") + if err != nil { + // Don't mask DB corruption behind silent defaults — log, then fall back. + slog.Warn("ai: load settings failed; using defaults", "error", err) + } + if s == nil { + s = &database.AISettings{ID: "default"} + } + if !ok || s.Provider == "" { + s.Provider = "anthropic" + } + if s.Model == "" { + s.Model = "claude-opus-4-8" + } + if s.ThinkingLevel == "" { + s.ThinkingLevel = "standard" + } + if s.ApprovalPolicy == "" { + s.ApprovalPolicy = "all_writes" + } + if s.MaxToolIterations <= 0 { + s.MaxToolIterations = 25 + } + if strings.TrimSpace(s.SystemPrompt) == "" { + s.SystemPrompt = defaultSystemPrompt + } + return *s +} + +// Settings returns the resolved settings for the API (with defaults applied). +func (m *Manager) Settings() database.AISettings { return m.resolvedSettings() } + +// SaveSettings persists settings, validating enums. +func (m *Manager) SaveSettings(in database.AISettings) (database.AISettings, error) { + in.ID = "default" + if !validThinking(in.ThinkingLevel) { + return database.AISettings{}, fmt.Errorf("invalid thinking_level %q", in.ThinkingLevel) + } + if !validApproval(in.ApprovalPolicy) { + return database.AISettings{}, fmt.Errorf("invalid approval_policy %q", in.ApprovalPolicy) + } + if err := m.db.UpsertSettings(&in); err != nil { + return database.AISettings{}, err + } + return m.resolvedSettings(), nil +} + +func validThinking(v string) bool { + switch v { + case "", "off", "standard", "deep": + return true + } + return false +} + +func validApproval(v string) bool { + switch v { + case "", "all_writes", "destructive_only", "auto": + return true + } + return false +} + +// ─── providers ─────────────────────────────────────────────────────────────── + +// ProviderView is the API projection of a provider config — the key is never +// returned, only whether one is set. +type ProviderView struct { + ID string `json:"id"` + Provider string `json:"provider"` + Label string `json:"label"` + HasKey bool `json:"has_key"` + BaseURL string `json:"base_url,omitempty"` + Enabled bool `json:"enabled"` +} + +func toProviderView(c *database.AIProviderConfig) ProviderView { + return ProviderView{ + ID: c.ID, Provider: c.Provider, Label: c.Label, + HasKey: c.APIKeyEncrypted != "", BaseURL: c.BaseURL, Enabled: c.Enabled, + } +} + +// ListProviders returns all configured providers (keys redacted). +func (m *Manager) ListProviders() ([]ProviderView, error) { + cfgs, err := m.db.ListProviderConfigs() + if err != nil { + return nil, err + } + out := make([]ProviderView, 0, len(cfgs)) + for _, c := range cfgs { + out = append(out, toProviderView(c)) + } + return out, nil +} + +// ProviderInput is the write payload for a provider config. APIKey is plaintext +// and only used on this request; an empty APIKey on update leaves the stored +// key untouched. +type ProviderInput struct { + Provider string `json:"provider"` + Label string `json:"label"` + APIKey string `json:"api_key"` + BaseURL string `json:"base_url"` + ExtraConfig string `json:"extra_config"` + Enabled *bool `json:"enabled"` +} + +// SaveProvider creates or updates a provider config, encrypting the key. +func (m *Manager) SaveProvider(in ProviderInput) (ProviderView, error) { + provider := strings.ToLower(strings.TrimSpace(in.Provider)) + if provider == "" { + return ProviderView{}, fmt.Errorf("provider is required") + } + cfg := &database.AIProviderConfig{ + Provider: provider, + Label: in.Label, + BaseURL: normalizeBaseURL(in.BaseURL), + ExtraConfig: in.ExtraConfig, + Enabled: in.Enabled == nil || *in.Enabled, + } + if strings.TrimSpace(in.APIKey) != "" { + enc, err := m.secrets.EncryptValue(in.APIKey) + if err != nil { + return ProviderView{}, err + } + cfg.APIKeyEncrypted = enc + } + saved, err := m.db.UpsertProviderConfig(cfg) + if err != nil { + return ProviderView{}, err + } + m.invalidateClient() // pick up the new/changed credentials + return toProviderView(saved), nil +} + +// DeleteProvider removes a provider config. +func (m *Manager) DeleteProvider(id string) error { + if err := m.db.DeleteProviderConfig(id); err != nil { + return err + } + m.invalidateClient() + return nil +} + +// ─── model catalog ─────────────────────────────────────────────────────────── + +// ModelInfo is one selectable model. +type ModelInfo struct { + ID string `json:"id"` + Label string `json:"label"` + Provider string `json:"provider"` +} + +// ProviderModels lists the models a configured provider actually exposes, by +// querying its OpenAI-compatible /v1/models endpoint with the stored +// credentials — fully dynamic, never hardcoded. Works for any OpenAI-compatible +// gateway (incl. self-hosted custom endpoints) and Anthropic. Returns the +// models plus a non-nil error string the UI can surface if the listing failed +// (so the user can fall back to typing a model id). +func (m *Manager) ProviderModels(id string) ([]ModelInfo, error) { + cfg, err := m.db.GetProviderConfig(id) + if err != nil { + return nil, err + } + key := "" + if cfg.APIKeyEncrypted != "" { + if k, derr := m.secrets.DecryptValue(cfg.APIKeyEncrypted); derr == nil { + key = k + } + } + root := normalizeBaseURL(cfg.BaseURL) + if root == "" { + root = defaultRoot(cfg.Provider) + } + if root == "" { + return nil, fmt.Errorf("no base URL configured for provider %q", cfg.Provider) + } + + req, err := http.NewRequest(http.MethodGet, root+"/v1/models", nil) + if err != nil { + return nil, err + } + if cfg.Provider == "anthropic" { + req.Header.Set("x-api-key", key) + req.Header.Set("anthropic-version", "2023-06-01") + } else if key != "" { + req.Header.Set("Authorization", "Bearer "+key) + } + resp, err := (&http.Client{Timeout: 15 * time.Second}).Do(req) + if err != nil { + return nil, fmt.Errorf("reach %s: %w", root, err) + } + defer resp.Body.Close() + body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("models endpoint returned %d", resp.StatusCode) + } + var parsed struct { + Data []struct { + ID string `json:"id"` + } `json:"data"` + } + if err := json.Unmarshal(body, &parsed); err != nil { + return nil, fmt.Errorf("parse models response: %w", err) + } + out := make([]ModelInfo, 0, len(parsed.Data)) + for _, d := range parsed.Data { + if d.ID != "" { + out = append(out, ModelInfo{ID: d.ID, Label: d.ID, Provider: cfg.Provider}) + } + } + return out, nil +} + +// normalizeBaseURL strips a trailing slash and a trailing "/v1" so the stored +// value is the provider ROOT. Bifrost (and our /v1/models probe) append the +// "/v1/…" path themselves, so a user can paste either "https://host" or the +// more natural "https://host/v1" and both work. +func normalizeBaseURL(u string) string { + u = strings.TrimSpace(u) + u = strings.TrimRight(u, "/") + u = strings.TrimSuffix(u, "/v1") + return strings.TrimRight(u, "/") +} + +// defaultRoot is the provider ROOT (no /v1) used when no custom base URL is set. +func defaultRoot(provider string) string { + switch provider { + case "openai": + return "https://api.openai.com" + case "groq": + return "https://api.groq.com/openai" + case "anthropic": + return "https://api.anthropic.com" + case "mistral": + return "https://api.mistral.ai" + case "openrouter": + return "https://openrouter.ai/api" + case "xai": + return "https://api.x.ai" + case "cohere": + return "https://api.cohere.ai/compatibility" + default: + return "" + } +} + +// ─── conversation passthrough (thin wrappers so the handler stays in one pkg) ─ + +func (m *Manager) ListConversations(userID string, includeArchived bool, limit, offset int) ([]*database.AIConversation, error) { + return m.db.ListConversations(userID, includeArchived, limit, offset) +} + +// ConversationDetail bundles a conversation with its messages + tool calls for +// rehydration on page load. +type ConversationDetail struct { + Conversation *database.AIConversation `json:"conversation"` + Messages []*database.AIMessage `json:"messages"` + ToolCalls []*database.AIToolCall `json:"tool_calls"` +} + +func (m *Manager) GetConversation(id string) (*ConversationDetail, error) { + c, err := m.db.GetConversation(id) + if err != nil { + return nil, err + } + msgs, err := m.db.ListMessages(id, 0) + if err != nil { + return nil, err + } + calls, err := m.db.ListToolCalls(id) + if err != nil { + return nil, err + } + return &ConversationDetail{Conversation: c, Messages: msgs, ToolCalls: calls}, nil +} + +func (m *Manager) CreateConversation(userID, title string) (*database.AIConversation, error) { + c := &database.AIConversation{UserID: userID, Title: title} + if err := m.db.CreateConversation(c); err != nil { + return nil, err + } + return c, nil +} + +func (m *Manager) UpdateConversation(id string, title *string, archived *bool) (*database.AIConversation, error) { + return m.db.UpdateConversation(id, title, archived) +} + +func (m *Manager) DeleteConversation(id string) error { return m.db.DeleteConversation(id) } + +func (m *Manager) ListMessages(convID string, sinceSeq int) ([]*database.AIMessage, error) { + return m.db.ListMessages(convID, sinceSeq) +} + +func (m *Manager) GetToolCall(id string) (*database.AIToolCall, error) { + return m.db.GetToolCall(id) +} + +// ─── helpers ───────────────────────────────────────────────────────────────── + +func firstNonEmpty(vals ...string) string { + for _, v := range vals { + if strings.TrimSpace(v) != "" { + return v + } + } + return "" +} + +func titleFrom(content string) string { + t := strings.TrimSpace(strings.SplitN(content, "\n", 2)[0]) + if len(t) > 60 { + t = t[:60] + "…" + } + if t == "" { + t = "New conversation" + } + return t +} diff --git a/backend/internal/ai/manager_test.go b/backend/internal/ai/manager_test.go new file mode 100644 index 0000000..3001dd9 --- /dev/null +++ b/backend/internal/ai/manager_test.go @@ -0,0 +1,32 @@ +package ai + +import "testing" + +// TestTryLockConv covers the per-conversation turn guard: one turn per +// conversation, independent across conversations, re-lockable after release. +// This is what prevents overlapping turns (double-send / chat-while-approving) +// from interleaving and corrupting message ordering. +func TestTryLockConv(t *testing.T) { + m := &Manager{} + + if !m.tryLockConv("c1") { + t.Fatal("first lock on c1 should succeed") + } + if m.tryLockConv("c1") { + t.Fatal("second lock on c1 should fail while a turn is in flight") + } + if !m.tryLockConv("c2") { + t.Fatal("a different conversation should lock independently of c1") + } + + m.unlockConv("c1") + if !m.tryLockConv("c1") { + t.Fatal("c1 should be lockable again after unlock") + } + + // c2 is still held; releasing it should also free it. + m.unlockConv("c2") + if !m.tryLockConv("c2") { + t.Fatal("c2 should be lockable again after unlock") + } +} diff --git a/backend/internal/database/ai_chat.go b/backend/internal/database/ai_chat.go new file mode 100644 index 0000000..401f35d --- /dev/null +++ b/backend/internal/database/ai_chat.go @@ -0,0 +1,619 @@ +package database + +import ( + "database/sql" + "errors" + "time" + + "github.com/Harsh-2002/Orva/internal/ids" +) + +// This file holds all persistence for the in-product AI chat agent: +// conversations, messages, tool calls, provider credentials, and settings. +// It follows the same convention as the rest of the database package — CRUD +// as methods on *Database, time.Time over DATETIME columns, UUIDv7 ids — so +// the AI feature's storage reads like every other resource here. + +// ErrAINotFound is returned when an AI row lookup misses. Callers can use +// errors.Is to disambiguate from real database errors. +var ErrAINotFound = errors.New("ai: not found") + +// ─── types ────────────────────────────────────────────────────────────── + +// AIConversation is one chat thread. provider/model snapshot what was +// active when the thread began; the live settings can drift independently. +type AIConversation struct { + ID string `json:"id"` + UserID string `json:"user_id,omitempty"` + Title string `json:"title"` + Provider string `json:"provider,omitempty"` + Model string `json:"model,omitempty"` + Archived bool `json:"archived"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// AIMessage is one turn in a conversation. content is the flattened text +// for fast render/search; parts is the JSON-encoded structured payload +// (text / code / thinking / tool_call blocks). Raw-JSON columns are kept +// as strings so the database layer stays decoupled from the agent's part +// types — higher layers marshal/unmarshal. +type AIMessage struct { + ID string `json:"id"` + ConversationID string `json:"conversation_id"` + Role string `json:"role"` // user | assistant | tool | system + Content string `json:"content"` + Parts string `json:"parts"` // JSON array + TokenUsage string `json:"token_usage,omitempty"` // JSON object + Seq int `json:"seq"` + CreatedAt time.Time `json:"created_at"` +} + +// AIToolCall records one tool invocation requested by the agent, its +// approval state, and its result. status drives the loop and the UI card. +type AIToolCall struct { + ID string `json:"id"` + ConversationID string `json:"conversation_id"` + MessageID string `json:"message_id,omitempty"` + CallID string `json:"call_id,omitempty"` // provider tool_call id + ToolName string `json:"tool_name"` + ToolGroup string `json:"tool_group,omitempty"` + Args string `json:"args"` // JSON object + Result string `json:"result,omitempty"` // JSON output or error + Status string `json:"status"` // pending_approval|approved|rejected|running|succeeded|failed + RequiresApproval bool `json:"requires_approval"` + Destructive bool `json:"destructive"` + ApprovedBy string `json:"approved_by,omitempty"` + StartedAt *time.Time `json:"started_at,omitempty"` + FinishedAt *time.Time `json:"finished_at,omitempty"` + DurationMS *int64 `json:"duration_ms,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + +// AIProviderConfig is a configured LLM provider + its (encrypted) API key. +// APIKeyEncrypted holds base64(nonce||ciphertext); it is never serialised +// to API clients (handlers strip it and report has_key instead). +type AIProviderConfig struct { + ID string `json:"id"` + Provider string `json:"provider"` + Label string `json:"label"` + APIKeyEncrypted string `json:"-"` + BaseURL string `json:"base_url,omitempty"` + ExtraConfig string `json:"extra_config,omitempty"` // JSON + Enabled bool `json:"enabled"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// AISettings is the singleton (or per-user) agent configuration row. +type AISettings struct { + ID string `json:"id"` + Provider string `json:"provider"` + Model string `json:"model"` + ThinkingLevel string `json:"thinking_level"` // off | standard | deep + SystemPrompt string `json:"system_prompt"` // empty → built-in default + ApprovalPolicy string `json:"approval_policy"` // all_writes | destructive_only | auto + MaxToolIterations int `json:"max_tool_iterations"` + UpdatedAt time.Time `json:"updated_at"` +} + +// ─── conversations ──────────────────────────────────────────────────────── + +func (db *Database) CreateConversation(c *AIConversation) error { + if c.ID == "" { + c.ID = ids.New() + } + if c.Title == "" { + c.Title = "New conversation" + } + now := time.Now().UTC() + c.CreatedAt = now + c.UpdatedAt = now + _, err := db.write.Exec(` + INSERT INTO ai_conversations (id, user_id, title, provider, model, archived, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + c.ID, nullStr(c.UserID), c.Title, nullStr(c.Provider), nullStr(c.Model), + boolToInt(c.Archived), c.CreatedAt, c.UpdatedAt, + ) + return err +} + +func (db *Database) GetConversation(id string) (*AIConversation, error) { + row := db.read.QueryRow(` + SELECT id, user_id, title, provider, model, archived, created_at, updated_at + FROM ai_conversations WHERE id = ?`, id) + c, err := scanConversation(row.Scan) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrAINotFound + } + return c, err +} + +// ListConversations returns threads ordered most-recently-updated first. +// userID="" returns all (single-user / admin view); a non-empty userID +// scopes to that principal. +func (db *Database) ListConversations(userID string, includeArchived bool, limit, offset int) ([]*AIConversation, error) { + if limit <= 0 || limit > 200 { + limit = 50 + } + q := `SELECT id, user_id, title, provider, model, archived, created_at, updated_at + FROM ai_conversations WHERE 1=1` + args := []any{} + if userID != "" { + q += " AND user_id = ?" + args = append(args, userID) + } + if !includeArchived { + q += " AND archived = 0" + } + q += " ORDER BY updated_at DESC LIMIT ? OFFSET ?" + args = append(args, limit, offset) + + rows, err := db.read.Query(q, args...) + if err != nil { + return nil, err + } + defer rows.Close() + out := make([]*AIConversation, 0) + for rows.Next() { + c, err := scanConversation(rows.Scan) + if err != nil { + return nil, err + } + out = append(out, c) + } + return out, rows.Err() +} + +// UpdateConversation patches title and/or archived. nil pointers are left +// untouched. updated_at is always bumped. +func (db *Database) UpdateConversation(id string, title *string, archived *bool) (*AIConversation, error) { + set := "updated_at = ?" + args := []any{time.Now().UTC()} + if title != nil { + set += ", title = ?" + args = append(args, *title) + } + if archived != nil { + set += ", archived = ?" + args = append(args, boolToInt(*archived)) + } + args = append(args, id) + res, err := db.write.Exec("UPDATE ai_conversations SET "+set+" WHERE id = ?", args...) + if err != nil { + return nil, err + } + if n, _ := res.RowsAffected(); n == 0 { + return nil, ErrAINotFound + } + return db.GetConversation(id) +} + +// TouchConversation bumps updated_at — called when a new message lands so +// the thread floats to the top of the list. +func (db *Database) TouchConversation(id string) error { + _, err := db.write.Exec(`UPDATE ai_conversations SET updated_at = ? WHERE id = ?`, + time.Now().UTC(), id) + return err +} + +// DeleteConversation removes a thread; messages + tool_calls cascade via FK. +// Idempotent — no error when the row never existed. +func (db *Database) DeleteConversation(id string) error { + _, err := db.write.Exec(`DELETE FROM ai_conversations WHERE id = ?`, id) + return err +} + +func scanConversation(scan func(...any) error) (*AIConversation, error) { + var ( + c AIConversation + userID sql.NullString + provider sql.NullString + model sql.NullString + archived int + ) + if err := scan(&c.ID, &userID, &c.Title, &provider, &model, &archived, &c.CreatedAt, &c.UpdatedAt); err != nil { + return nil, err + } + c.UserID = userID.String + c.Provider = provider.String + c.Model = model.String + c.Archived = archived != 0 + return &c, nil +} + +// ─── messages ───────────────────────────────────────────────────────────── + +// InsertMessage appends a message, assigning id, the next per-conversation +// seq, and created_at. The write pool is single-connection so the +// MAX(seq)+1 read-then-insert is race-free. +func (db *Database) InsertMessage(m *AIMessage) error { + if m.ID == "" { + m.ID = ids.New() + } + if m.Parts == "" { + m.Parts = "[]" + } + m.CreatedAt = time.Now().UTC() + if m.Seq == 0 { + // Atomic seq assignment: derive MAX(seq)+1 inside the INSERT so the + // read-and-write is a single statement. The write pool is a single + // connection (SetMaxOpenConns(1)), so one statement cannot interleave + // with another writer — making concurrent inserts on the same + // conversation race-free. (A prior SELECT-MAX-then-INSERT was two + // round-trips and could hand two messages the same seq.) + _, err := db.write.Exec(` + INSERT INTO ai_messages (id, conversation_id, role, content, parts, token_usage, seq, created_at) + VALUES (?, ?, ?, ?, ?, ?, + (SELECT COALESCE(MAX(seq), 0) + 1 FROM ai_messages WHERE conversation_id = ?), ?)`, + m.ID, m.ConversationID, m.Role, m.Content, m.Parts, nullStr(m.TokenUsage), m.ConversationID, m.CreatedAt, + ) + return err + } + _, err := db.write.Exec(` + INSERT INTO ai_messages (id, conversation_id, role, content, parts, token_usage, seq, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + m.ID, m.ConversationID, m.Role, m.Content, m.Parts, nullStr(m.TokenUsage), m.Seq, m.CreatedAt, + ) + return err +} + +// UpdateMessage rewrites the content/parts/token_usage of an existing +// message — used to finalise an assistant message after streaming. +func (db *Database) UpdateMessage(id, content, parts, tokenUsage string) error { + if parts == "" { + parts = "[]" + } + _, err := db.write.Exec( + `UPDATE ai_messages SET content = ?, parts = ?, token_usage = ? WHERE id = ?`, + content, parts, nullStr(tokenUsage), id) + return err +} + +// ListMessages returns a conversation's messages in order. sinceSeq > 0 +// returns only messages after that seq (incremental fetch). +func (db *Database) ListMessages(conversationID string, sinceSeq int) ([]*AIMessage, error) { + rows, err := db.read.Query(` + SELECT id, conversation_id, role, content, parts, token_usage, seq, created_at + FROM ai_messages + WHERE conversation_id = ? AND seq > ? + ORDER BY seq ASC`, conversationID, sinceSeq) + if err != nil { + return nil, err + } + defer rows.Close() + out := make([]*AIMessage, 0) + for rows.Next() { + var ( + m AIMessage + tokenUsage sql.NullString + ) + if err := rows.Scan(&m.ID, &m.ConversationID, &m.Role, &m.Content, &m.Parts, &tokenUsage, &m.Seq, &m.CreatedAt); err != nil { + return nil, err + } + m.TokenUsage = tokenUsage.String + out = append(out, &m) + } + return out, rows.Err() +} + +// GetMessage returns a single message by id. +func (db *Database) GetMessage(id string) (*AIMessage, error) { + var ( + m AIMessage + tokenUsage sql.NullString + ) + err := db.read.QueryRow(` + SELECT id, conversation_id, role, content, parts, token_usage, seq, created_at + FROM ai_messages WHERE id = ?`, id).Scan( + &m.ID, &m.ConversationID, &m.Role, &m.Content, &m.Parts, &tokenUsage, &m.Seq, &m.CreatedAt) + if err != nil { + return nil, err + } + m.TokenUsage = tokenUsage.String + return &m, nil +} + +// DeleteMessagesFromSeq removes a conversation's messages with seq >= fromSeq +// (and the tool calls those messages own), truncating the conversation. Used by +// regenerate, edit-and-resend, and delete-from-here. Runs in one transaction so +// history never ends up half-truncated. +func (db *Database) DeleteMessagesFromSeq(conversationID string, fromSeq int) error { + tx, err := db.write.Begin() + if err != nil { + return err + } + defer tx.Rollback() + if _, err := tx.Exec(` + DELETE FROM ai_tool_calls + WHERE conversation_id = ? AND message_id IN ( + SELECT id FROM ai_messages WHERE conversation_id = ? AND seq >= ? + )`, conversationID, conversationID, fromSeq); err != nil { + return err + } + if _, err := tx.Exec( + `DELETE FROM ai_messages WHERE conversation_id = ? AND seq >= ?`, + conversationID, fromSeq); err != nil { + return err + } + return tx.Commit() +} + +// ─── tool calls ─────────────────────────────────────────────────────────── + +func (db *Database) InsertToolCall(t *AIToolCall) error { + if t.ID == "" { + t.ID = ids.New() + } + if t.Args == "" { + t.Args = "{}" + } + if t.Status == "" { + t.Status = "pending_approval" + } + t.CreatedAt = time.Now().UTC() + _, err := db.write.Exec(` + INSERT INTO ai_tool_calls + (id, conversation_id, message_id, call_id, tool_name, tool_group, args, result, + status, requires_approval, destructive, approved_by, started_at, finished_at, duration_ms, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + t.ID, t.ConversationID, nullStr(t.MessageID), nullStr(t.CallID), t.ToolName, t.ToolGroup, + t.Args, nullStr(t.Result), t.Status, boolToInt(t.RequiresApproval), boolToInt(t.Destructive), + nullStr(t.ApprovedBy), t.StartedAt, t.FinishedAt, t.DurationMS, t.CreatedAt, + ) + return err +} + +func (db *Database) GetToolCall(id string) (*AIToolCall, error) { + row := db.read.QueryRow(` + SELECT id, conversation_id, message_id, call_id, tool_name, tool_group, args, result, + status, requires_approval, destructive, approved_by, started_at, finished_at, duration_ms, created_at + FROM ai_tool_calls WHERE id = ?`, id) + t, err := scanToolCall(row.Scan) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrAINotFound + } + return t, err +} + +// UpdateToolCall persists status/result/approval/timing transitions. Only +// the supplied fields move; pass the current values for the rest (the loop +// holds the row in memory between transitions). +func (db *Database) UpdateToolCall(t *AIToolCall) error { + res, err := db.write.Exec(` + UPDATE ai_tool_calls + SET status = ?, result = ?, approved_by = ?, started_at = ?, finished_at = ?, duration_ms = ? + WHERE id = ?`, + t.Status, nullStr(t.Result), nullStr(t.ApprovedBy), t.StartedAt, t.FinishedAt, t.DurationMS, t.ID, + ) + if err != nil { + return err + } + if n, _ := res.RowsAffected(); n == 0 { + return ErrAINotFound + } + return nil +} + +// ListToolCalls returns all tool calls for a conversation (rehydration). +func (db *Database) ListToolCalls(conversationID string) ([]*AIToolCall, error) { + rows, err := db.read.Query(` + SELECT id, conversation_id, message_id, call_id, tool_name, tool_group, args, result, + status, requires_approval, destructive, approved_by, started_at, finished_at, duration_ms, created_at + FROM ai_tool_calls WHERE conversation_id = ? ORDER BY created_at ASC`, conversationID) + if err != nil { + return nil, err + } + defer rows.Close() + out := make([]*AIToolCall, 0) + for rows.Next() { + t, err := scanToolCall(rows.Scan) + if err != nil { + return nil, err + } + out = append(out, t) + } + return out, rows.Err() +} + +func scanToolCall(scan func(...any) error) (*AIToolCall, error) { + var ( + t AIToolCall + messageID, callID sql.NullString + result, approvedBy sql.NullString + requiresApproval, destruct int + started, finished sql.NullTime + durationMS sql.NullInt64 + ) + if err := scan(&t.ID, &t.ConversationID, &messageID, &callID, &t.ToolName, &t.ToolGroup, + &t.Args, &result, &t.Status, &requiresApproval, &destruct, &approvedBy, + &started, &finished, &durationMS, &t.CreatedAt); err != nil { + return nil, err + } + t.MessageID = messageID.String + t.CallID = callID.String + t.Result = result.String + t.ApprovedBy = approvedBy.String + t.RequiresApproval = requiresApproval != 0 + t.Destructive = destruct != 0 + if started.Valid { + t.StartedAt = &started.Time + } + if finished.Valid { + t.FinishedAt = &finished.Time + } + if durationMS.Valid { + v := durationMS.Int64 + t.DurationMS = &v + } + return &t, nil +} + +// ─── provider configs ───────────────────────────────────────────────────── + +// UpsertProviderConfig inserts or updates a provider config keyed by +// (provider, label). The encrypted key is only overwritten when non-empty, +// so a PATCH that omits the key rotates nothing. +func (db *Database) UpsertProviderConfig(c *AIProviderConfig) (*AIProviderConfig, error) { + if c.ID == "" { + c.ID = ids.New() + } + now := time.Now().UTC() + if c.CreatedAt.IsZero() { + c.CreatedAt = now + } + c.UpdatedAt = now + _, err := db.write.Exec(` + INSERT INTO ai_provider_configs + (id, provider, label, api_key_encrypted, base_url, extra_config, enabled, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(provider, label) DO UPDATE SET + api_key_encrypted = CASE WHEN excluded.api_key_encrypted != '' THEN excluded.api_key_encrypted ELSE ai_provider_configs.api_key_encrypted END, + base_url = excluded.base_url, + extra_config = excluded.extra_config, + enabled = excluded.enabled, + updated_at = excluded.updated_at`, + c.ID, c.Provider, c.Label, c.APIKeyEncrypted, nullStr(c.BaseURL), nullStr(c.ExtraConfig), + boolToInt(c.Enabled), c.CreatedAt, c.UpdatedAt, + ) + if err != nil { + return nil, err + } + return db.GetProviderConfigByKey(c.Provider, c.Label) +} + +func (db *Database) GetProviderConfig(id string) (*AIProviderConfig, error) { + row := db.read.QueryRow(providerSelect+` WHERE id = ?`, id) + c, err := scanProviderConfig(row.Scan) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrAINotFound + } + return c, err +} + +func (db *Database) GetProviderConfigByKey(provider, label string) (*AIProviderConfig, error) { + row := db.read.QueryRow(providerSelect+` WHERE provider = ? AND label = ?`, provider, label) + c, err := scanProviderConfig(row.Scan) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrAINotFound + } + return c, err +} + +// GetEnabledProviderConfig returns the first enabled config for a provider, +// preferring the most recently updated. Used by the LLM account adapter to +// resolve the key for an inbound chat request. +func (db *Database) GetEnabledProviderConfig(provider string) (*AIProviderConfig, error) { + row := db.read.QueryRow(providerSelect+ + ` WHERE provider = ? AND enabled = 1 ORDER BY updated_at DESC LIMIT 1`, provider) + c, err := scanProviderConfig(row.Scan) + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrAINotFound + } + return c, err +} + +func (db *Database) ListProviderConfigs() ([]*AIProviderConfig, error) { + rows, err := db.read.Query(providerSelect + ` ORDER BY provider ASC, label ASC`) + if err != nil { + return nil, err + } + defer rows.Close() + out := make([]*AIProviderConfig, 0) + for rows.Next() { + c, err := scanProviderConfig(rows.Scan) + if err != nil { + return nil, err + } + out = append(out, c) + } + return out, rows.Err() +} + +func (db *Database) DeleteProviderConfig(id string) error { + _, err := db.write.Exec(`DELETE FROM ai_provider_configs WHERE id = ?`, id) + return err +} + +const providerSelect = `SELECT id, provider, label, api_key_encrypted, base_url, extra_config, enabled, created_at, updated_at FROM ai_provider_configs` + +func scanProviderConfig(scan func(...any) error) (*AIProviderConfig, error) { + var ( + c AIProviderConfig + apiKey sql.NullString + baseURL, extraConfig sql.NullString + enabled int + ) + if err := scan(&c.ID, &c.Provider, &c.Label, &apiKey, &baseURL, &extraConfig, &enabled, &c.CreatedAt, &c.UpdatedAt); err != nil { + return nil, err + } + c.APIKeyEncrypted = apiKey.String + c.BaseURL = baseURL.String + c.ExtraConfig = extraConfig.String + c.Enabled = enabled != 0 + return &c, nil +} + +// ─── settings ───────────────────────────────────────────────────────────── + +// GetSettings returns the row for id (use "default" for the instance-wide +// row). When the row is missing it returns a zero-value AISettings with the +// id set and ok=false, so callers can apply built-in defaults without a +// separate not-found dance. +func (db *Database) GetSettings(id string) (*AISettings, bool, error) { + row := db.read.QueryRow(` + SELECT id, provider, model, thinking_level, system_prompt, approval_policy, max_tool_iterations, updated_at + FROM ai_settings WHERE id = ?`, id) + var ( + s AISettings + systemPrompt sql.NullString + ) + err := row.Scan(&s.ID, &s.Provider, &s.Model, &s.ThinkingLevel, &systemPrompt, &s.ApprovalPolicy, &s.MaxToolIterations, &s.UpdatedAt) + if errors.Is(err, sql.ErrNoRows) { + return &AISettings{ID: id}, false, nil + } + if err != nil { + return nil, false, err + } + s.SystemPrompt = systemPrompt.String + return &s, true, nil +} + +// UpsertSettings writes the settings row by id. +func (db *Database) UpsertSettings(s *AISettings) error { + if s.ID == "" { + s.ID = "default" + } + if s.MaxToolIterations <= 0 { + s.MaxToolIterations = 25 + } + s.UpdatedAt = time.Now().UTC() + _, err := db.write.Exec(` + INSERT INTO ai_settings + (id, provider, model, thinking_level, system_prompt, approval_policy, max_tool_iterations, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + provider = excluded.provider, + model = excluded.model, + thinking_level = excluded.thinking_level, + system_prompt = excluded.system_prompt, + approval_policy = excluded.approval_policy, + max_tool_iterations = excluded.max_tool_iterations, + updated_at = excluded.updated_at`, + s.ID, s.Provider, s.Model, s.ThinkingLevel, nullStr(s.SystemPrompt), s.ApprovalPolicy, + s.MaxToolIterations, s.UpdatedAt, + ) + return err +} + +// ─── small helpers ──────────────────────────────────────────────────────── + +// nullStr maps "" → NULL so empty optional columns store as NULL rather +// than empty string (keeps scans symmetric with sql.NullString). +// (boolToInt is shared from blocklist.go.) +func nullStr(s string) any { + if s == "" { + return nil + } + return s +} diff --git a/backend/internal/database/keys.go b/backend/internal/database/keys.go index 8da645d..5c224bc 100644 --- a/backend/internal/database/keys.go +++ b/backend/internal/database/keys.go @@ -74,6 +74,28 @@ func (db *Database) GetAPIKeyByHash(hash string) (*APIKey, error) { return &key, nil } +// GetAPIKeyByID looks up a key by its id. Used by the AI chat handler to +// resolve an API-key caller's permission set when scoping the agent's tool +// catalog. +func (db *Database) GetAPIKeyByID(id string) (*APIKey, error) { + var key APIKey + var lastUsed, expires sql.NullTime + err := db.read.QueryRow(` + SELECT id, key_hash, COALESCE(key_prefix, ''), name, permissions, created_at, last_used_at, expires_at + FROM api_keys WHERE id = ?`, id, + ).Scan(&key.ID, &key.KeyHash, &key.Prefix, &key.Name, &key.Permissions, &key.CreatedAt, &lastUsed, &expires) + if err != nil { + return nil, err + } + if lastUsed.Valid { + key.LastUsedAt = &lastUsed.Time + } + if expires.Valid { + key.ExpiresAt = &expires.Time + } + return &key, nil +} + func (db *Database) ListAPIKeys() ([]*APIKey, error) { rows, err := db.read.Query(` SELECT id, key_hash, COALESCE(key_prefix, ''), name, permissions, created_at, last_used_at, expires_at diff --git a/backend/internal/database/migrations.go b/backend/internal/database/migrations.go index 99c8704..c85a2d8 100644 --- a/backend/internal/database/migrations.go +++ b/backend/internal/database/migrations.go @@ -432,6 +432,84 @@ INSERT OR IGNORE INTO egress_blocklist (kind, rule_type, value, label, enabled) ('suggested', 'cidr', '192.168.0.0/16', 'Private network (RFC1918)', 0), ('suggested', 'cidr', '100.64.0.0/10', 'CGNAT / Tailscale', 0); +-- ─── AI chat agent (in-product assistant) ────────────────────────────── +-- Conversations, messages, tool calls, provider credentials, and settings +-- for the dashboard's AI chat. Internal tool calls reuse the MCP impl +-- functions in-process; provider API keys are encrypted at rest with the +-- same AES-256-GCM cipher as function_secrets (see secrets.Manager). +CREATE TABLE IF NOT EXISTS ai_conversations ( + id TEXT PRIMARY KEY, + user_id TEXT, -- principal id (session user / api key id); null on single-user installs + title TEXT NOT NULL DEFAULT 'New conversation', + provider TEXT, -- provider snapshot at creation (e.g. 'anthropic') + model TEXT, -- model snapshot + archived INTEGER NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_ai_conv_user ON ai_conversations(user_id); +CREATE INDEX IF NOT EXISTS idx_ai_conv_updated ON ai_conversations(updated_at DESC); + +CREATE TABLE IF NOT EXISTS ai_messages ( + id TEXT PRIMARY KEY, + conversation_id TEXT NOT NULL, + role TEXT NOT NULL, -- user | assistant | tool | system + content TEXT NOT NULL DEFAULT '', -- flattened text (fast render / search) + parts TEXT NOT NULL DEFAULT '[]', -- JSON array: [{type:text|code|thinking|tool_call, …}] + token_usage TEXT, -- JSON {prompt, completion, reasoning} + seq INTEGER NOT NULL, -- monotonic per-conversation ordering + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (conversation_id) REFERENCES ai_conversations(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_ai_msg_conv ON ai_messages(conversation_id, seq); + +CREATE TABLE IF NOT EXISTS ai_tool_calls ( + id TEXT PRIMARY KEY, + conversation_id TEXT NOT NULL, + message_id TEXT, -- assistant message that requested it + call_id TEXT, -- provider-assigned tool_call id (for result correlation) + tool_name TEXT NOT NULL, + tool_group TEXT NOT NULL DEFAULT '', -- functions | deploy | secrets | … + args TEXT NOT NULL DEFAULT '{}', + result TEXT, -- JSON output or error string + status TEXT NOT NULL DEFAULT 'pending_approval', + -- pending_approval | approved | rejected | running | succeeded | failed + requires_approval INTEGER NOT NULL DEFAULT 0, + destructive INTEGER NOT NULL DEFAULT 0, + approved_by TEXT, -- principal id who approved + started_at DATETIME, + finished_at DATETIME, + duration_ms INTEGER, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (conversation_id) REFERENCES ai_conversations(id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_ai_tc_conv ON ai_tool_calls(conversation_id); +CREATE INDEX IF NOT EXISTS idx_ai_tc_status ON ai_tool_calls(status); + +CREATE TABLE IF NOT EXISTS ai_provider_configs ( + id TEXT PRIMARY KEY, + provider TEXT NOT NULL, -- openai | anthropic | bedrock | gemini | groq | ollama | … + label TEXT NOT NULL DEFAULT '', + api_key_encrypted TEXT, -- base64(nonce||ciphertext), AES-256-GCM via secrets.Manager + base_url TEXT, -- optional override (Azure / Ollama / self-host) + extra_config TEXT, -- JSON (e.g. AWS region for Bedrock) + enabled INTEGER NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(provider, label) +); + +CREATE TABLE IF NOT EXISTS ai_settings ( + id TEXT PRIMARY KEY, -- 'default' or user_id + provider TEXT NOT NULL DEFAULT 'anthropic', + model TEXT NOT NULL DEFAULT 'claude-opus-4-8', + thinking_level TEXT NOT NULL DEFAULT 'standard', -- off | standard | deep + system_prompt TEXT, -- nullable; falls back to the built-in operator prompt + approval_policy TEXT NOT NULL DEFAULT 'all_writes', -- all_writes | destructive_only | auto + max_tool_iterations INTEGER NOT NULL DEFAULT 25, -- agentic loop cap + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + PRAGMA foreign_keys = ON; ` if _, err := db.write.Exec(schema); err != nil { diff --git a/backend/internal/mcp/agent_registry.go b/backend/internal/mcp/agent_registry.go new file mode 100644 index 0000000..7db0413 --- /dev/null +++ b/backend/internal/mcp/agent_registry.go @@ -0,0 +1,189 @@ +package mcp + +import ( + "context" + "encoding/json" + "fmt" + "sort" + + "github.com/google/jsonschema-go/jsonschema" + mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// agent_registry.go gives Orva's in-product AI agent an IN-PROCESS view of +// the exact same tools the external MCP server exposes — without any MCP +// transport, JSON-RPC, or HTTP. The agent calls the tool implementations +// directly as Go functions. +// +// Single source of truth: every tool is declared once via regAddTool, which +// (a) registers it with the external *mcpsdk.Server (unchanged behaviour, +// for claude.ai / ChatGPT connectors) AND (b) records an AgentTool in the +// in-process Registry the chat agent dispatches against. Same name, same +// LLM-tuned description, same JSON schema (inferred from the same input +// struct + jsonschema tags), same permission gating, same destructive +// hints — so the two fronts can never drift. + +// regCtx carries everything a register*Tools function needs. It is built two +// ways: +// - server.go (external MCP): srv set, reg nil → tools registered with the SDK. +// - BuildAgentRegistry (internal agent): srv nil, reg set → tools recorded +// for in-process dispatch only. +type regCtx struct { + srv *mcpsdk.Server + deps Deps + perms permSet + reg *Registry + group string // current domain label applied to tools added next (e.g. "functions") +} + +// serverRegCtx builds a regCtx for the external MCP server path. +func serverRegCtx(s *mcpsdk.Server, deps Deps, perms permSet) *regCtx { + return ®Ctx{srv: s, deps: deps, perms: perms} +} + +// AgentTool is one tool the in-process agent can call. Schema is the JSON +// schema for the tool's arguments (inferred from the input struct); Invoke +// runs the tool implementation directly and returns its typed output. +type AgentTool struct { + Name string `json:"name"` + Description string `json:"description"` + Group string `json:"group"` + Perm string `json:"perm"` // read | write | invoke | admin + ReadOnly bool `json:"read_only"` + Destructive bool `json:"destructive"` + Schema json.RawMessage `json:"schema"` // JSON Schema for arguments + + invoke func(ctx context.Context, args json.RawMessage) (any, error) +} + +// Registry is the agent's tool catalog. It is built per principal so it only +// ever contains tools that principal's permissions allow — the same guarantee +// the MCP server gives. +type Registry struct { + order []string + byName map[string]*AgentTool +} + +func newRegistry() *Registry { return &Registry{byName: map[string]*AgentTool{}} } + +func (r *Registry) add(t *AgentTool) { + if _, dup := r.byName[t.Name]; dup { + return + } + r.byName[t.Name] = t + r.order = append(r.order, t.Name) +} + +// Tools returns the catalog in stable (sorted) order. +func (r *Registry) Tools() []*AgentTool { + out := make([]*AgentTool, 0, len(r.order)) + for _, n := range r.order { + out = append(out, r.byName[n]) + } + sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) + return out +} + +// Get returns the tool by name, or nil. +func (r *Registry) Get(name string) *AgentTool { return r.byName[name] } + +// Dispatch runs a tool by name with raw JSON arguments and returns its typed +// output. The output is whatever the underlying impl returns (the agent loop +// JSON-encodes it back to the model). Returns an error for unknown tools or +// failed invocations. +func (r *Registry) Dispatch(ctx context.Context, name string, args json.RawMessage) (any, error) { + t := r.byName[name] + if t == nil { + return nil, fmt.Errorf("unknown tool %q", name) + } + return t.invoke(ctx, args) +} + +// regAddTool declares one tool. It is generic over the input/output types so +// the JSON schema is inferred from In and the handler is type-checked, exactly +// like mcpsdk.AddTool. Pass the existing handler closure verbatim — no need to +// extract inline closures. +func regAddTool[In, Out any]( + rc *regCtx, + perm string, + def *mcpsdk.Tool, + h mcpsdk.ToolHandlerFor[In, Out], +) { + if !rc.perms.Has(perm) { + return // permission-gated: invisible to this principal on both fronts + } + + // External MCP server path — byte-for-byte the old behaviour. + if rc.srv != nil { + mcpsdk.AddTool(rc.srv, def, h) + } + + // In-process agent registry path. + if rc.reg != nil { + var schemaJSON json.RawMessage + if s, err := jsonschema.For[In](nil); err == nil && s != nil { + if b, err := json.Marshal(s); err == nil { + schemaJSON = b + } + } + readOnly := def.Annotations != nil && def.Annotations.ReadOnlyHint + destructive := def.Annotations != nil && + def.Annotations.DestructiveHint != nil && *def.Annotations.DestructiveHint + rc.reg.add(&AgentTool{ + Name: def.Name, + Description: def.Description, + Group: rc.group, + Perm: perm, + ReadOnly: readOnly, + Destructive: destructive, + Schema: schemaJSON, + invoke: func(ctx context.Context, args json.RawMessage) (any, error) { + var in In + if len(args) > 0 { + if err := json.Unmarshal(args, &in); err != nil { + return nil, fmt.Errorf("invalid arguments for %s: %w", def.Name, err) + } + } + _, out, err := h(ctx, nil, in) + if err != nil { + return nil, err + } + return out, nil + }, + }) + } +} + +// BuildAgentRegistry assembles the in-process tool catalog for a principal. +// Only register*Tools families that have been migrated to regAddTool appear +// here; each call is gated to the principal's perms inside regAddTool, so a +// read-only key yields a read-only catalog. +// +// As more tool files are migrated to regAddTool, add their register call here. +func BuildAgentRegistry(deps Deps, perms permSet) *Registry { + reg := newRegistry() + rc := ®Ctx{deps: deps, perms: perms, reg: reg} + + // Every operator-tool family, gated to this principal's perms inside + // regAddTool. This mirrors the external MCP server's registration in + // server.go — same tools, same descriptions, same schemas. + registerSystemTools(rc) + registerDocsTools(rc) + registerFunctionTools(rc) + registerDeployTools(rc) + registerInvokeTools(rc) + registerSecretTools(rc) + registerRouteTools(rc) + registerKeyTools(rc) + registerFirewallTools(rc) + registerPoolTools(rc) + registerCronTools(rc) + registerKVTools(rc) + registerJobTools(rc) + registerWebhookTools(rc) + registerInboundWebhookTools(rc) + registerFixtureTools(rc) + registerTraceTools(rc) + + return reg +} diff --git a/backend/internal/mcp/agent_registry_test.go b/backend/internal/mcp/agent_registry_test.go new file mode 100644 index 0000000..af8b02c --- /dev/null +++ b/backend/internal/mcp/agent_registry_test.go @@ -0,0 +1,65 @@ +package mcp + +import ( + "encoding/json" + "testing" +) + +// TestBuildAgentRegistrySecrets verifies that the migrated secret tools land +// in the in-process registry with the expected metadata + an inferred JSON +// schema, and that permission gating filters the catalog. +func TestBuildAgentRegistrySecrets(t *testing.T) { + perms := permSet{"read": true, "write": true} + reg := BuildAgentRegistry(Deps{}, perms) + + want := map[string]struct { + perm string + destructive bool + }{ + "list_secrets": {perm: "read", destructive: false}, + "set_secret": {perm: "write", destructive: false}, + "delete_secret": {perm: "write", destructive: true}, + } + + for name, exp := range want { + tool := reg.Get(name) + if tool == nil { + t.Fatalf("expected tool %q in registry", name) + } + if tool.Perm != exp.perm { + t.Errorf("%s: perm = %q, want %q", name, tool.Perm, exp.perm) + } + if tool.Destructive != exp.destructive { + t.Errorf("%s: destructive = %v, want %v", name, tool.Destructive, exp.destructive) + } + if tool.Group != "secrets" { + t.Errorf("%s: group = %q, want secrets", name, tool.Group) + } + if len(tool.Schema) == 0 { + t.Errorf("%s: empty schema", name) + } + // The schema must be a valid JSON object describing function_id etc. + var obj map[string]any + if err := json.Unmarshal(tool.Schema, &obj); err != nil { + t.Errorf("%s: schema not valid JSON: %v", name, err) + } + if obj["type"] != "object" { + t.Errorf("%s: schema type = %v, want object", name, obj["type"]) + } + } +} + +// TestBuildAgentRegistryPermGating confirms a read-only principal sees only +// read tools — never write/destructive ones. +func TestBuildAgentRegistryPermGating(t *testing.T) { + reg := BuildAgentRegistry(Deps{}, permSet{"read": true}) + if reg.Get("list_secrets") == nil { + t.Error("read principal should see list_secrets") + } + if reg.Get("set_secret") != nil { + t.Error("read principal must NOT see set_secret") + } + if reg.Get("delete_secret") != nil { + t.Error("read principal must NOT see delete_secret") + } +} diff --git a/backend/internal/mcp/schema_portability_test.go b/backend/internal/mcp/schema_portability_test.go index 23cff75..ee1e4ef 100644 --- a/backend/internal/mcp/schema_portability_test.go +++ b/backend/internal/mcp/schema_portability_test.go @@ -25,24 +25,25 @@ func TestSchemaPortability(t *testing.T) { deps := Deps{} // empty deps — handlers won't run; we only inspect schemas perms := allPerms() - // Register every operator-mode tool surface. - registerSystemTools(srv, deps, perms) - registerFunctionTools(srv, deps, perms) - registerInvokeTools(srv, deps, perms) - registerDeployTools(srv, deps, perms) - registerSecretTools(srv, deps, perms) - registerRouteTools(srv, deps, perms) - registerFixtureTools(srv, deps, perms) - registerFirewallTools(srv, deps, perms) - registerWebhookTools(srv, deps, perms) - registerInboundWebhookTools(srv, deps, perms) - registerKeyTools(srv, deps, perms) - registerKVTools(srv, deps, perms) - registerJobTools(srv, deps, perms) - registerCronTools(srv, deps, perms) - registerPoolTools(srv, deps, perms) - registerTraceTools(srv, deps, perms) - registerDocsTools(srv, deps, perms) + // Register every operator-mode tool surface (via the shared regCtx). + rc := serverRegCtx(srv, deps, perms) + registerSystemTools(rc) + registerFunctionTools(rc) + registerInvokeTools(rc) + registerDeployTools(rc) + registerSecretTools(rc) + registerRouteTools(rc) + registerFixtureTools(rc) + registerFirewallTools(rc) + registerWebhookTools(rc) + registerInboundWebhookTools(rc) + registerKeyTools(rc) + registerKVTools(rc) + registerJobTools(rc) + registerCronTools(rc) + registerPoolTools(rc) + registerTraceTools(rc) + registerDocsTools(rc) // Connect via an in-memory transport so we can hit the public // ListTools API without needing a real network listener. diff --git a/backend/internal/mcp/server.go b/backend/internal/mcp/server.go index 5a30195..0b5e25c 100644 --- a/backend/internal/mcp/server.go +++ b/backend/internal/mcp/server.go @@ -136,30 +136,34 @@ func NewHandler(deps Deps) http.Handler { // only show the streaming request itself. s.AddReceivingMiddleware(activityMiddleware(reqDeps, principal)) - registerSystemTools(s, reqDeps, perms) - registerFunctionTools(s, reqDeps, perms) - registerDeployTools(s, reqDeps, perms) - registerInvokeTools(s, reqDeps, perms) - registerSecretTools(s, reqDeps, perms) - registerRouteTools(s, reqDeps, perms) - registerKeyTools(s, reqDeps, perms) - registerFirewallTools(s, reqDeps, perms) - registerPoolTools(s, reqDeps, perms) + // Every operator-tool family is registered through a regCtx so the + // exact same tool definitions feed both the external MCP server (here) + // and the in-process AI agent registry (BuildAgentRegistry). + rc := serverRegCtx(s, reqDeps, perms) + registerSystemTools(rc) + registerFunctionTools(rc) + registerDeployTools(rc) + registerInvokeTools(rc) + registerSecretTools(rc) + registerRouteTools(rc) + registerKeyTools(rc) + registerFirewallTools(rc) + registerPoolTools(rc) // v0.2 + v0.3: cron schedules, KV store, background jobs, and // system-event webhooks. Each respects the same permission gates. - registerCronTools(s, reqDeps, perms) - registerKVTools(s, reqDeps, perms) - registerJobTools(s, reqDeps, perms) - registerWebhookTools(s, reqDeps, perms) + registerCronTools(rc) + registerKVTools(rc) + registerJobTools(rc) + registerWebhookTools(rc) // v0.4 C2a: inbound webhook triggers (signed external POSTs). - registerInboundWebhookTools(s, reqDeps, perms) + registerInboundWebhookTools(rc) // v0.4 B3 + B5: saved request fixtures (Postman-style presets) + // test_function_with_fixture invoke variant. - registerFixtureTools(s, reqDeps, perms) + registerFixtureTools(rc) // v0.5: causal tracing — get_trace / list_traces / get_function_baseline. - registerTraceTools(s, reqDeps, perms) + registerTraceTools(rc) // v0.5: get_orva_docs — return the canonical Orva reference markdown. - registerDocsTools(s, reqDeps, perms) + registerDocsTools(rc) registerResources(s, reqDeps, perms) diff --git a/backend/internal/mcp/tools_cron.go b/backend/internal/mcp/tools_cron.go index ed80aab..10ab77e 100644 --- a/backend/internal/mcp/tools_cron.go +++ b/backend/internal/mcp/tools_cron.go @@ -25,15 +25,15 @@ type CronView struct { LastRunAt string `json:"last_run_at,omitempty"` NextRunAt string `json:"next_run_at,omitempty"` LastStatus string `json:"last_status,omitempty"` - LastError string `json:"last_error,omitempty"` + LastError string `json:"last_error,omitempty"` // Payload is the JSON object delivered as the invoke body when the // schedule fires. Declared as map[string]any (rather than `any`) so // the SDK emits a JSON Schema {type: "object"} that strict Zod v4 // validators in MCP clients accept — `any` would emit `true`, the // JSON-Schema-spec shorthand for "any value", which Zod rejects as // a non-object schema and crashes tools/list parsing. - Payload map[string]any `json:"payload"` - CreatedAt string `json:"created_at"` + Payload map[string]any `json:"payload"` + CreatedAt string `json:"created_at"` } func toCronView(s *database.CronSchedule, fnName string) CronView { @@ -82,16 +82,16 @@ type ListCronOutput struct { } type CreateCronInput struct { - FunctionID string `json:"function_id" jsonschema:"function id (UUID) or name"` - CronExpr string `json:"cron_expr" jsonschema:"5-field cron expression. Supports @daily / @hourly / @weekly / @monthly / @yearly shorthands"` - Enabled *bool `json:"enabled,omitempty" jsonschema:"defaults to true"` + FunctionID string `json:"function_id" jsonschema:"function id (UUID) or name"` + CronExpr string `json:"cron_expr" jsonschema:"5-field cron expression. Supports @daily / @hourly / @weekly / @monthly / @yearly shorthands"` + Enabled *bool `json:"enabled,omitempty" jsonschema:"defaults to true"` Payload map[string]any `json:"payload,omitempty" jsonschema:"JSON object delivered as the invoke body when the schedule fires; default {}"` } type UpdateCronInput struct { - ID string `json:"id" jsonschema:"schedule id (cron_...)"` - CronExpr string `json:"cron_expr,omitempty" jsonschema:"new cron expression; omit to keep"` - Enabled *bool `json:"enabled,omitempty" jsonschema:"new enabled flag; omit to keep"` + ID string `json:"id" jsonschema:"schedule id (cron_...)"` + CronExpr string `json:"cron_expr,omitempty" jsonschema:"new cron expression; omit to keep"` + Enabled *bool `json:"enabled,omitempty" jsonschema:"new enabled flag; omit to keep"` Payload map[string]any `json:"payload,omitempty" jsonschema:"new payload; omit to keep"` } @@ -104,160 +104,155 @@ type CronOpOutput struct { ID string `json:"id"` } -func registerCronTools(s *mcpsdk.Server, deps Deps, perms permSet) { - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "list_cron_schedules", - Title: "List Cron Schedules", - Description: "List cron schedules. Pass function_id to filter by function (id or name); omit to list every schedule. Times are RFC3339; last_status is 'ok' / 'failed' / empty.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in ListCronInput) (*mcpsdk.CallToolResult, ListCronOutput, error) { - out := ListCronOutput{Schedules: []CronView{}} - if strings.TrimSpace(in.FunctionID) != "" { - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, out, err - } - rows, err := deps.DB.ListCronSchedulesForFunction(fn.ID) - if err != nil { - return nil, out, err - } - for _, r := range rows { - out.Schedules = append(out.Schedules, toCronView(r, fn.Name)) - } - return nil, out, nil +func registerCronTools(rc *regCtx) { + deps := rc.deps + rc.group = "cron" + + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "list_cron_schedules", + Title: "List Cron Schedules", + Description: "List cron schedules. Pass function_id to filter by function (id or name); omit to list every schedule. Times are RFC3339; last_status is 'ok' / 'failed' / empty.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in ListCronInput) (*mcpsdk.CallToolResult, ListCronOutput, error) { + out := ListCronOutput{Schedules: []CronView{}} + if strings.TrimSpace(in.FunctionID) != "" { + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, out, err } - rows, err := deps.DB.ListAllCronSchedulesWithFunction() + rows, err := deps.DB.ListCronSchedulesForFunction(fn.ID) if err != nil { return nil, out, err } for _, r := range rows { - out.Schedules = append(out.Schedules, toCronView(r.CronSchedule, r.FunctionName)) + out.Schedules = append(out.Schedules, toCronView(r, fn.Name)) } return nil, out, nil - }, - ) - }) + } + rows, err := deps.DB.ListAllCronSchedulesWithFunction() + if err != nil { + return nil, out, err + } + for _, r := range rows { + out.Schedules = append(out.Schedules, toCronView(r.CronSchedule, r.FunctionName)) + } + return nil, out, nil + }, + ) - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "create_cron_schedule", - Title: "Create Cron Schedule", - Description: "Schedule a function to fire on a cron expression. Returns the new schedule with next_run_at filled in. The schedule is stored centrally; the orvad scheduler picks it up on its next 30s tick.", - Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: false, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in CreateCronInput) (*mcpsdk.CallToolResult, CronView, error) { - expr := strings.TrimSpace(in.CronExpr) - if expr == "" { - return nil, CronView{}, errors.New("cron_expr is required") - } - sched, err := scheduler.ParseCronExpr(expr) + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "create_cron_schedule", + Title: "Create Cron Schedule", + Description: "Schedule a function to fire on a cron expression. Returns the new schedule with next_run_at filled in. The schedule is stored centrally; the orvad scheduler picks it up on its next 30s tick.", + Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: false, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in CreateCronInput) (*mcpsdk.CallToolResult, CronView, error) { + expr := strings.TrimSpace(in.CronExpr) + if expr == "" { + return nil, CronView{}, errors.New("cron_expr is required") + } + sched, err := scheduler.ParseCronExpr(expr) + if err != nil { + return nil, CronView{}, errors.New("invalid cron_expr: " + err.Error()) + } + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, CronView{}, err + } + enabled := true + if in.Enabled != nil { + enabled = *in.Enabled + } + payload := "{}" + if in.Payload != nil { + b, err := json.Marshal(in.Payload) if err != nil { + return nil, CronView{}, errors.New("payload must be JSON-serializable") + } + payload = string(b) + } + row := &database.CronSchedule{ + FunctionID: fn.ID, + CronExpr: expr, + Enabled: enabled, + Payload: payload, + } + next := sched.Next(time.Now().UTC()) + row.NextRunAt = &next + if err := deps.DB.InsertCronSchedule(row); err != nil { + return nil, CronView{}, err + } + return nil, toCronView(row, fn.Name), nil + }, + ) + + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "update_cron_schedule", + Title: "Update Cron Schedule", + Description: "Edit an existing cron schedule. Any of cron_expr / enabled / payload may be supplied; omitted fields keep their previous values. next_run_at is recomputed when the expression changes.", + Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in UpdateCronInput) (*mcpsdk.CallToolResult, CronView, error) { + row, err := deps.DB.GetCronSchedule(in.ID) + if err != nil { + return nil, CronView{}, errors.New("schedule not found") + } + exprChanged := false + if expr := strings.TrimSpace(in.CronExpr); expr != "" && expr != row.CronExpr { + if _, err := scheduler.ParseCronExpr(expr); err != nil { return nil, CronView{}, errors.New("invalid cron_expr: " + err.Error()) } - fn, err := resolveFunction(deps, in.FunctionID) + row.CronExpr = expr + exprChanged = true + } + if in.Enabled != nil { + row.Enabled = *in.Enabled + } + if in.Payload != nil { + b, err := json.Marshal(in.Payload) if err != nil { - return nil, CronView{}, err - } - enabled := true - if in.Enabled != nil { - enabled = *in.Enabled - } - payload := "{}" - if in.Payload != nil { - b, err := json.Marshal(in.Payload) - if err != nil { - return nil, CronView{}, errors.New("payload must be JSON-serializable") - } - payload = string(b) - } - row := &database.CronSchedule{ - FunctionID: fn.ID, - CronExpr: expr, - Enabled: enabled, - Payload: payload, + return nil, CronView{}, errors.New("payload must be JSON-serializable") } + row.Payload = string(b) + } + if exprChanged || row.Enabled { + sched, _ := scheduler.ParseCronExpr(row.CronExpr) next := sched.Next(time.Now().UTC()) row.NextRunAt = &next - if err := deps.DB.InsertCronSchedule(row); err != nil { - return nil, CronView{}, err - } - return nil, toCronView(row, fn.Name), nil - }, - ) - }) - - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "update_cron_schedule", - Title: "Update Cron Schedule", - Description: "Edit an existing cron schedule. Any of cron_expr / enabled / payload may be supplied; omitted fields keep their previous values. next_run_at is recomputed when the expression changes.", - Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in UpdateCronInput) (*mcpsdk.CallToolResult, CronView, error) { - row, err := deps.DB.GetCronSchedule(in.ID) - if err != nil { - return nil, CronView{}, errors.New("schedule not found") - } - exprChanged := false - if expr := strings.TrimSpace(in.CronExpr); expr != "" && expr != row.CronExpr { - if _, err := scheduler.ParseCronExpr(expr); err != nil { - return nil, CronView{}, errors.New("invalid cron_expr: " + err.Error()) - } - row.CronExpr = expr - exprChanged = true - } - if in.Enabled != nil { - row.Enabled = *in.Enabled - } - if in.Payload != nil { - b, err := json.Marshal(in.Payload) - if err != nil { - return nil, CronView{}, errors.New("payload must be JSON-serializable") - } - row.Payload = string(b) - } - if exprChanged || row.Enabled { - sched, _ := scheduler.ParseCronExpr(row.CronExpr) - next := sched.Next(time.Now().UTC()) - row.NextRunAt = &next - } - if err := deps.DB.UpdateCronSchedule(row); err != nil { - return nil, CronView{}, err - } - fnName := "" - if fn, err := deps.DB.GetFunction(row.FunctionID); err == nil { - fnName = fn.Name - } - return nil, toCronView(row, fnName), nil - }, - ) - }) + } + if err := deps.DB.UpdateCronSchedule(row); err != nil { + return nil, CronView{}, err + } + fnName := "" + if fn, err := deps.DB.GetFunction(row.FunctionID); err == nil { + fnName = fn.Name + } + return nil, toCronView(row, fnName), nil + }, + ) - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "delete_cron_schedule", - Title: "Delete Cron Schedule", - Description: "Permanently remove a cron schedule by id; the targeted function itself stays untouched and remains invokable. Pass confirm=true to acknowledge the removal. If the operator wants to pause the schedule temporarily rather than delete it, prefer update_cron_schedule with enabled=false — that preserves the cron expression and payload for later resume.", - Annotations: &mcpsdk.ToolAnnotations{DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in DeleteCronInput) (*mcpsdk.CallToolResult, CronOpOutput, error) { - if !in.Confirm { - return nil, CronOpOutput{}, errors.New("delete refused: pass confirm=true") - } - if _, err := deps.DB.GetCronSchedule(in.ID); err != nil { - return nil, CronOpOutput{}, errors.New("schedule not found") - } - if err := deps.DB.DeleteCronSchedule(in.ID); err != nil { - return nil, CronOpOutput{}, err - } - return nil, CronOpOutput{ID: in.ID}, nil - }, - ) - }) + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "delete_cron_schedule", + Title: "Delete Cron Schedule", + Description: "Permanently remove a cron schedule by id; the targeted function itself stays untouched and remains invokable. Pass confirm=true to acknowledge the removal. If the operator wants to pause the schedule temporarily rather than delete it, prefer update_cron_schedule with enabled=false — that preserves the cron expression and payload for later resume.", + Annotations: &mcpsdk.ToolAnnotations{DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in DeleteCronInput) (*mcpsdk.CallToolResult, CronOpOutput, error) { + if !in.Confirm { + return nil, CronOpOutput{}, errors.New("delete refused: pass confirm=true") + } + if _, err := deps.DB.GetCronSchedule(in.ID); err != nil { + return nil, CronOpOutput{}, errors.New("schedule not found") + } + if err := deps.DB.DeleteCronSchedule(in.ID); err != nil { + return nil, CronOpOutput{}, err + } + return nil, CronOpOutput{ID: in.ID}, nil + }, + ) } diff --git a/backend/internal/mcp/tools_deploy.go b/backend/internal/mcp/tools_deploy.go index 163b250..23bdb4c 100644 --- a/backend/internal/mcp/tools_deploy.go +++ b/backend/internal/mcp/tools_deploy.go @@ -132,145 +132,136 @@ type WaitDeploymentInput struct { } // registerDeployTools wires the deploy / rollback / inspect tools. -func registerDeployTools(s *mcpsdk.Server, deps Deps, perms permSet) { - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "get_deployment", - Title: "Get Deployment", - Description: "Fetch a single deployment by id. Returns status (queued/building/succeeded/failed), phase, code_hash, duration, and any error message. Use this to poll a deployment that's in flight.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in GetDeploymentInput) (*mcpsdk.CallToolResult, DeploymentView, error) { - dep, err := deps.DB.GetDeployment(in.DeploymentID) - if err != nil { - return nil, DeploymentView{}, fmt.Errorf("deployment not found: %s", in.DeploymentID) - } - return nil, toDeploymentView(dep), nil - }, - ) - }) +func registerDeployTools(rc *regCtx) { + deps := rc.deps + rc.group = "deploy" + + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "get_deployment", + Title: "Get Deployment", + Description: "Fetch a single deployment by id. Returns status (queued/building/succeeded/failed), phase, code_hash, duration, and any error message. Use this to poll a deployment that's in flight.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in GetDeploymentInput) (*mcpsdk.CallToolResult, DeploymentView, error) { + dep, err := deps.DB.GetDeployment(in.DeploymentID) + if err != nil { + return nil, DeploymentView{}, fmt.Errorf("deployment not found: %s", in.DeploymentID) + } + return nil, toDeploymentView(dep), nil + }, + ) - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "get_deployment_logs", - Title: "Get Deployment Logs", - Description: "Read the build log for a deployment. Each line has a monotonically-increasing seq — pass from= to tail incrementally. Empty list = no new lines. Use this for diagnosing build failures.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in GetDeploymentLogsInput) (*mcpsdk.CallToolResult, GetDeploymentLogsOutput, error) { - lim := in.Limit - if lim <= 0 { - lim = 200 - } - if lim > 2000 { - lim = 2000 - } - lines, err := deps.DB.GetBuildLogs(in.DeploymentID, in.From, lim) - if err != nil { - return nil, GetDeploymentLogsOutput{}, err - } - out := GetDeploymentLogsOutput{Logs: make([]DeploymentLogLine, 0, len(lines))} - for _, ln := range lines { - out.Logs = append(out.Logs, DeploymentLogLine{Seq: ln.Seq, Stream: ln.Stream, Text: ln.Line, At: ln.TS}) - } - return nil, out, nil - }, - ) - }) + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "get_deployment_logs", + Title: "Get Deployment Logs", + Description: "Read the build log for a deployment. Each line has a monotonically-increasing seq — pass from= to tail incrementally. Empty list = no new lines. Use this for diagnosing build failures.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in GetDeploymentLogsInput) (*mcpsdk.CallToolResult, GetDeploymentLogsOutput, error) { + lim := in.Limit + if lim <= 0 { + lim = 200 + } + if lim > 2000 { + lim = 2000 + } + lines, err := deps.DB.GetBuildLogs(in.DeploymentID, in.From, lim) + if err != nil { + return nil, GetDeploymentLogsOutput{}, err + } + out := GetDeploymentLogsOutput{Logs: make([]DeploymentLogLine, 0, len(lines))} + for _, ln := range lines { + out.Logs = append(out.Logs, DeploymentLogLine{Seq: ln.Seq, Stream: ln.Stream, Text: ln.Line, At: ln.TS}) + } + return nil, out, nil + }, + ) - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "list_function_deployments", - Title: "List Function Deployments", - Description: "List a function's deployment history (newest first). Each entry has the deployment id, code_hash, status, and timestamps — use it to find a target for rollback_function.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in ListFunctionDeploymentsInput) (*mcpsdk.CallToolResult, ListFunctionDeploymentsOutput, error) { - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, ListFunctionDeploymentsOutput{}, err - } - lim := in.Limit - if lim <= 0 { - lim = 50 - } - deplist, err := deps.DB.ListDeploymentsForFunction(fn.ID, lim) - if err != nil { - return nil, ListFunctionDeploymentsOutput{}, err - } - out := ListFunctionDeploymentsOutput{Deployments: make([]DeploymentView, 0, len(deplist))} - for _, d := range deplist { - out.Deployments = append(out.Deployments, toDeploymentView(d)) - } - return nil, out, nil - }, - ) - }) + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "list_function_deployments", + Title: "List Function Deployments", + Description: "List a function's deployment history (newest first). Each entry has the deployment id, code_hash, status, and timestamps — use it to find a target for rollback_function.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in ListFunctionDeploymentsInput) (*mcpsdk.CallToolResult, ListFunctionDeploymentsOutput, error) { + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, ListFunctionDeploymentsOutput{}, err + } + lim := in.Limit + if lim <= 0 { + lim = 50 + } + deplist, err := deps.DB.ListDeploymentsForFunction(fn.ID, lim) + if err != nil { + return nil, ListFunctionDeploymentsOutput{}, err + } + out := ListFunctionDeploymentsOutput{Deployments: make([]DeploymentView, 0, len(deplist))} + for _, d := range deplist { + out.Deployments = append(out.Deployments, toDeploymentView(d)) + } + return nil, out, nil + }, + ) - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "deploy_function_inline", - Title: "Deploy Function Inline", - Description: "BEFORE calling this: if you have not already called `get_orva_docs` in this conversation, call it first. Orva's handler contract, SDK shape, and event envelope diverge from Lambda / Vercel / Cloudflare Workers; agents that skip the docs and rely on training-data defaults consistently produce code that fails at first invoke (`require('orva')` not `import orva`, `kv.put` not `kv.set`, `exports.handler` not `export default`, no `event.query` parsing of arbitrary URLs, etc.). Reading the docs once costs ~3 KB and prevents 4-6 round-trips of trial-and-error rebuilds.\n\n" + - "Deploy source code to a function. Pass the full handler file as `code`, optional dependency-file content as `dependencies`. `wait` is REQUIRED — pass true to block until the build terminates (so invoke_function won't race a still-queued build), or false to return immediately with status='queued' and poll yourself. Returns deployment_id either way; carries a `warning` field when the source imports the orva SDK but the function's network_mode is 'none' (the SDK call would fail at runtime).", - Annotations: &mcpsdk.ToolAnnotations{ - DestructiveHint: ptrFalse(), - OpenWorldHint: ptrFalse(), - }, + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "deploy_function_inline", + Title: "Deploy Function Inline", + Description: "BEFORE calling this: if you have not already called `get_orva_docs` in this conversation, call it first. Orva's handler contract, SDK shape, and event envelope diverge from Lambda / Vercel / Cloudflare Workers; agents that skip the docs and rely on training-data defaults consistently produce code that fails at first invoke (`require('orva')` not `import orva`, `kv.put` not `kv.set`, `exports.handler` not `export default`, no `event.query` parsing of arbitrary URLs, etc.). Reading the docs once costs ~3 KB and prevents 4-6 round-trips of trial-and-error rebuilds.\n\n" + + "Deploy source code to a function. Pass the full handler file as `code`, optional dependency-file content as `dependencies`. `wait` is REQUIRED — pass true to block until the build terminates (so invoke_function won't race a still-queued build), or false to return immediately with status='queued' and poll yourself. Returns deployment_id either way; carries a `warning` field when the source imports the orva SDK but the function's network_mode is 'none' (the SDK call would fail at runtime).", + Annotations: &mcpsdk.ToolAnnotations{ + DestructiveHint: ptrFalse(), + OpenWorldHint: ptrFalse(), }, - func(ctx context.Context, _ *mcpsdk.CallToolRequest, in DeployInlineInput) (*mcpsdk.CallToolResult, DeployInlineOutput, error) { - return deployInline(ctx, deps, in) - }, - ) - }) + }, + func(ctx context.Context, _ *mcpsdk.CallToolRequest, in DeployInlineInput) (*mcpsdk.CallToolResult, DeployInlineOutput, error) { + return deployInline(ctx, deps, in) + }, + ) - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "wait_deployment", - Title: "Wait Deployment", - Description: "Block until a deployment reaches a terminal state (succeeded or failed) or the timeout fires. Useful after deploy_function_inline with wait=false. Default timeout 120 s, max 600 s.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(ctx context.Context, _ *mcpsdk.CallToolRequest, in WaitDeploymentInput) (*mcpsdk.CallToolResult, DeploymentView, error) { - timeout := in.TimeoutSeconds - if timeout <= 0 { - timeout = 120 - } - if timeout > 600 { - timeout = 600 - } - return nil, waitDeployment(ctx, deps, in.DeploymentID, time.Duration(timeout)*time.Second), nil - }, - ) - }) + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "wait_deployment", + Title: "Wait Deployment", + Description: "Block until a deployment reaches a terminal state (succeeded or failed) or the timeout fires. Useful after deploy_function_inline with wait=false. Default timeout 120 s, max 600 s.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(ctx context.Context, _ *mcpsdk.CallToolRequest, in WaitDeploymentInput) (*mcpsdk.CallToolResult, DeploymentView, error) { + timeout := in.TimeoutSeconds + if timeout <= 0 { + timeout = 120 + } + if timeout > 600 { + timeout = 600 + } + return nil, waitDeployment(ctx, deps, in.DeploymentID, time.Duration(timeout)*time.Second), nil + }, + ) - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "rollback_function", - Title: "Rollback Function", - Description: "Roll a function back to a prior succeeded deployment. Pass deployment_id (preferred — restores the env_vars + spawn config snapshot from that deploy) or code_hash. Pass confirm=true. Secrets are NOT versioned and remain at current values.", - Annotations: &mcpsdk.ToolAnnotations{ - DestructiveHint: ptrTrue(), - OpenWorldHint: ptrFalse(), - }, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in RollbackFunctionInput) (*mcpsdk.CallToolResult, RollbackFunctionOutput, error) { - if !in.Confirm { - return nil, RollbackFunctionOutput{}, errors.New("rollback refused: pass confirm=true") - } - if in.DeploymentID == "" && in.CodeHash == "" { - return nil, RollbackFunctionOutput{}, errors.New("either deployment_id or code_hash is required") - } - return rollbackFunction(deps, in) + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "rollback_function", + Title: "Rollback Function", + Description: "Roll a function back to a prior succeeded deployment. Pass deployment_id (preferred — restores the env_vars + spawn config snapshot from that deploy) or code_hash. Pass confirm=true. Secrets are NOT versioned and remain at current values.", + Annotations: &mcpsdk.ToolAnnotations{ + DestructiveHint: ptrTrue(), + OpenWorldHint: ptrFalse(), }, - ) - }) + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in RollbackFunctionInput) (*mcpsdk.CallToolResult, RollbackFunctionOutput, error) { + if !in.Confirm { + return nil, RollbackFunctionOutput{}, errors.New("rollback refused: pass confirm=true") + } + if in.DeploymentID == "" && in.CodeHash == "" { + return nil, RollbackFunctionOutput{}, errors.New("either deployment_id or code_hash is required") + } + return rollbackFunction(deps, in) + }, + ) } // ─── implementations ─────────────────────────────────────────────── diff --git a/backend/internal/mcp/tools_docs.go b/backend/internal/mcp/tools_docs.go index 2bc8400..f21f71a 100644 --- a/backend/internal/mcp/tools_docs.go +++ b/backend/internal/mcp/tools_docs.go @@ -41,32 +41,31 @@ type GetOrvaDocsOutput struct { // registerDocsTools wires get_orva_docs into the per-request server. // Read permission is sufficient — the docs are public reference // material and exposing them never grants any escalated capability. -func registerDocsTools(s *mcpsdk.Server, deps Deps, perms permSet) { - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "get_orva_docs", - Title: "Get Orva Docs", - Description: "Call this BEFORE writing or modifying handler code on Orva. The handler contract, in-sandbox SDK, and event envelope diverge from Lambda / Vercel / Cloudflare Workers; skipping the docs is the leading source of agent-deployed handlers that fail at first invoke. Specifically the docs cover the rules an agent's training-data defaults will get wrong: require('orva') (CommonJS, not ESM `import`), kv.put (not kv.set), exports.handler = async (event) => ... (not `export default`), the explicit event envelope shape (event.method / event.path / event.headers / event.body / event.query), network_mode rules, auth_mode rules.\n\n" + - "Returns the complete Orva reference as a single Markdown string — the same content the dashboard's 'Copy as Markdown' icon serves. Covers handler contract, deploy/invoke, configuration, the in-sandbox SDK (KV / invoke / jobs), schedules, system-event webhooks, MCP setup, the AI codegen system prompt, tracing, error taxonomy, and the orva CLI. Pass `origin` (e.g. https://orva.example.com) to substitute the {{ORIGIN}} placeholders with the caller's live Orva URL; defaults to a generic placeholder so the response is still pasteable.", - Annotations: &mcpsdk.ToolAnnotations{ - ReadOnlyHint: true, - OpenWorldHint: ptrFalse(), - }, +func registerDocsTools(rc *regCtx) { + rc.group = "docs" + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "get_orva_docs", + Title: "Get Orva Docs", + Description: "Call this BEFORE writing or modifying handler code on Orva. The handler contract, in-sandbox SDK, and event envelope diverge from Lambda / Vercel / Cloudflare Workers; skipping the docs is the leading source of agent-deployed handlers that fail at first invoke. Specifically the docs cover the rules an agent's training-data defaults will get wrong: require('orva') (CommonJS, not ESM `import`), kv.put (not kv.set), exports.handler = async (event) => ... (not `export default`), the explicit event envelope shape (event.method / event.path / event.headers / event.body / event.query), network_mode rules, auth_mode rules.\n\n" + + "Returns the complete Orva reference as a single Markdown string — the same content the dashboard's 'Copy as Markdown' icon serves. Covers handler contract, deploy/invoke, configuration, the in-sandbox SDK (KV / invoke / jobs), schedules, system-event webhooks, MCP setup, the AI codegen system prompt, tracing, error taxonomy, and the orva CLI. Pass `origin` (e.g. https://orva.example.com) to substitute the {{ORIGIN}} placeholders with the caller's live Orva URL; defaults to a generic placeholder so the response is still pasteable.", + Annotations: &mcpsdk.ToolAnnotations{ + ReadOnlyHint: true, + OpenWorldHint: ptrFalse(), }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in GetOrvaDocsInput) (*mcpsdk.CallToolResult, GetOrvaDocsOutput, error) { - origin := strings.TrimRight(strings.TrimSpace(in.Origin), "/") - if origin == "" { - origin = "https://your-orva-instance.example.com" - } - md := strings.ReplaceAll(orvaDocsMarkdown, "{{ORIGIN}}", origin) - return nil, GetOrvaDocsOutput{ - Markdown: md, - ByteCount: len(md), - Origin: origin, - Note: "Use this as the source of truth when answering questions about Orva.", - }, nil - }, - ) - }) + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in GetOrvaDocsInput) (*mcpsdk.CallToolResult, GetOrvaDocsOutput, error) { + origin := strings.TrimRight(strings.TrimSpace(in.Origin), "/") + if origin == "" { + origin = "https://your-orva-instance.example.com" + } + md := strings.ReplaceAll(orvaDocsMarkdown, "{{ORIGIN}}", origin) + return nil, GetOrvaDocsOutput{ + Markdown: md, + ByteCount: len(md), + Origin: origin, + Note: "Use this as the source of truth when answering questions about Orva.", + }, nil + }, + ) } diff --git a/backend/internal/mcp/tools_firewall.go b/backend/internal/mcp/tools_firewall.go index a957c2a..16e901b 100644 --- a/backend/internal/mcp/tools_firewall.go +++ b/backend/internal/mcp/tools_firewall.go @@ -61,173 +61,165 @@ type SetDNSConfigInput struct { Records []DNSRecordView `json:"records,omitempty" jsonschema:"host→IP overrides (max 64) — beats upstream DNS"` } -func registerFirewallTools(s *mcpsdk.Server, deps Deps, perms permSet) { - gatedAdd(perms, permAdmin, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "list_firewall_rules", - Title: "List Firewall Rules", - Description: "List all egress firewall rules — both built-in defaults and operator-added customs. Each rule is a CIDR, hostname, or wildcard pattern; enabled rules block matching outbound traffic from sandboxes with network_mode=egress.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, ListFirewallRulesOutput, error) { - rows, err := deps.DB.ListBlocklistRules() - if err != nil { - return nil, ListFirewallRulesOutput{}, err - } - out := ListFirewallRulesOutput{Rules: make([]FirewallRuleView, 0, len(rows))} - for _, r := range rows { - out.Rules = append(out.Rules, toFirewallRuleView(r)) - } - return nil, out, nil - }, - ) - }) +func registerFirewallTools(rc *regCtx) { + deps := rc.deps + rc.group = "firewall" + regAddTool(rc, permAdmin, + &mcpsdk.Tool{ + Name: "list_firewall_rules", + Title: "List Firewall Rules", + Description: "List all egress firewall rules — both built-in defaults and operator-added customs. Each rule is a CIDR, hostname, or wildcard pattern; enabled rules block matching outbound traffic from sandboxes with network_mode=egress.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, ListFirewallRulesOutput, error) { + rows, err := deps.DB.ListBlocklistRules() + if err != nil { + return nil, ListFirewallRulesOutput{}, err + } + out := ListFirewallRulesOutput{Rules: make([]FirewallRuleView, 0, len(rows))} + for _, r := range rows { + out.Rules = append(out.Rules, toFirewallRuleView(r)) + } + return nil, out, nil + }, + ) - gatedAdd(perms, permAdmin, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "add_firewall_rule", - Title: "Add Firewall Rule", - Description: "Add a custom egress firewall rule. Value can be a CIDR (10.0.0.0/8), hostname (example.com), or wildcard (*.example.com) — type is auto-detected. Takes effect immediately for new sandbox spawns.", - Annotations: &mcpsdk.ToolAnnotations{OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in AddFirewallRuleInput) (*mcpsdk.CallToolResult, FirewallRuleView, error) { - value := strings.TrimSpace(in.Value) - if value == "" { - return nil, FirewallRuleView{}, errors.New("value is required") - } - ruleType := in.RuleType - if ruleType == "" { - switch { - case strings.Contains(value, "/"): - ruleType = database.BlocklistTypeCIDR - case strings.HasPrefix(value, "*."): - ruleType = database.BlocklistTypeWildcard - default: - ruleType = database.BlocklistTypeHostname - } - } - if !database.ValidBlocklistRuleType(ruleType) { - return nil, FirewallRuleView{}, errors.New("invalid rule_type (allowed: cidr, hostname, wildcard)") - } - enabled := true - if in.Enabled != nil { - enabled = *in.Enabled - } - rule, err := deps.DB.InsertCustomBlocklistRule(ruleType, value, in.Label, enabled) - if err != nil { - return nil, FirewallRuleView{}, err - } - if deps.Firewall != nil { - _ = deps.Firewall.ForceRefresh() - } - return nil, toFirewallRuleView(rule), nil - }, - ) - }) + regAddTool(rc, permAdmin, + &mcpsdk.Tool{ + Name: "add_firewall_rule", + Title: "Add Firewall Rule", + Description: "Add a custom egress firewall rule. Value can be a CIDR (10.0.0.0/8), hostname (example.com), or wildcard (*.example.com) — type is auto-detected. Takes effect immediately for new sandbox spawns.", + Annotations: &mcpsdk.ToolAnnotations{OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in AddFirewallRuleInput) (*mcpsdk.CallToolResult, FirewallRuleView, error) { + value := strings.TrimSpace(in.Value) + if value == "" { + return nil, FirewallRuleView{}, errors.New("value is required") + } + ruleType := in.RuleType + if ruleType == "" { + switch { + case strings.Contains(value, "/"): + ruleType = database.BlocklistTypeCIDR + case strings.HasPrefix(value, "*."): + ruleType = database.BlocklistTypeWildcard + default: + ruleType = database.BlocklistTypeHostname + } + } + if !database.ValidBlocklistRuleType(ruleType) { + return nil, FirewallRuleView{}, errors.New("invalid rule_type (allowed: cidr, hostname, wildcard)") + } + enabled := true + if in.Enabled != nil { + enabled = *in.Enabled + } + rule, err := deps.DB.InsertCustomBlocklistRule(ruleType, value, in.Label, enabled) + if err != nil { + return nil, FirewallRuleView{}, err + } + if deps.Firewall != nil { + _ = deps.Firewall.ForceRefresh() + } + return nil, toFirewallRuleView(rule), nil + }, + ) - gatedAdd(perms, permAdmin, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "delete_firewall_rule", - Title: "Delete Firewall Rule", - Description: "Delete a custom firewall rule by id. Built-in (default/suggested) rules can't be deleted — disable them via add_firewall_rule's enabled flag instead. Pass confirm=true.", - Annotations: &mcpsdk.ToolAnnotations{DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in DeleteFirewallRuleInput) (*mcpsdk.CallToolResult, DeletedOutput, error) { - if !in.Confirm { - return nil, DeletedOutput{}, errors.New("delete refused: pass confirm=true") - } - if err := deps.DB.DeleteCustomBlocklistRule(in.RuleID); err != nil { - return nil, DeletedOutput{}, err - } - if deps.Firewall != nil { - _ = deps.Firewall.ForceRefresh() - } - return nil, DeletedOutput{DeletedID: ""}, nil - }, - ) - }) + regAddTool(rc, permAdmin, + &mcpsdk.Tool{ + Name: "delete_firewall_rule", + Title: "Delete Firewall Rule", + Description: "Delete a custom firewall rule by id. Built-in (default/suggested) rules can't be deleted — disable them via add_firewall_rule's enabled flag instead. Pass confirm=true.", + Annotations: &mcpsdk.ToolAnnotations{DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in DeleteFirewallRuleInput) (*mcpsdk.CallToolResult, DeletedOutput, error) { + if !in.Confirm { + return nil, DeletedOutput{}, errors.New("delete refused: pass confirm=true") + } + if err := deps.DB.DeleteCustomBlocklistRule(in.RuleID); err != nil { + return nil, DeletedOutput{}, err + } + if deps.Firewall != nil { + _ = deps.Firewall.ForceRefresh() + } + return nil, DeletedOutput{DeletedID: ""}, nil + }, + ) - gatedAdd(perms, permAdmin, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "get_dns_config", - Title: "Get DNS Config", - Description: "Get the operator-managed DNS configuration: upstream resolver IPs, optional search domain, and host→IP overrides. Sandboxes with network_mode=egress see this as their /etc/resolv.conf and /etc/hosts.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, GetDNSConfigOutput, error) { - cfg := firewall.LoadDNSConfig(deps.DB) - out := GetDNSConfigOutput{ - Servers: cfg.Servers, Search: cfg.Search, Defaults: cfg.Defaults, - Records: make([]DNSRecordView, 0, len(cfg.Records)), - } - for _, r := range cfg.Records { - out.Records = append(out.Records, DNSRecordView{Host: r.Host, IP: r.IP}) - } - return nil, out, nil - }, - ) - }) + regAddTool(rc, permAdmin, + &mcpsdk.Tool{ + Name: "get_dns_config", + Title: "Get DNS Config", + Description: "Get the operator-managed DNS configuration: upstream resolver IPs, optional search domain, and host→IP overrides. Sandboxes with network_mode=egress see this as their /etc/resolv.conf and /etc/hosts.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, GetDNSConfigOutput, error) { + cfg := firewall.LoadDNSConfig(deps.DB) + out := GetDNSConfigOutput{ + Servers: cfg.Servers, Search: cfg.Search, Defaults: cfg.Defaults, + Records: make([]DNSRecordView, 0, len(cfg.Records)), + } + for _, r := range cfg.Records { + out.Records = append(out.Records, DNSRecordView{Host: r.Host, IP: r.IP}) + } + return nil, out, nil + }, + ) - gatedAdd(perms, permAdmin, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "set_dns_config", - Title: "Set DNS Config", - Description: "Update DNS settings. Servers must be literal IPs (not hostnames). Records (max 64) override DNS for specific hostnames. Idempotent — pass the desired full state.", - Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in SetDNSConfigInput) (*mcpsdk.CallToolResult, GetDNSConfigOutput, error) { - // Validate servers — must be IPs. - cleanServers := []string{} - for _, sIP := range in.Servers { - sIP = strings.TrimSpace(sIP) - if sIP == "" { - continue - } - if net.ParseIP(sIP) == nil { - return nil, GetDNSConfigOutput{}, errors.New("dns server must be a literal IP: " + sIP) - } - cleanServers = append(cleanServers, sIP) - } - if err := deps.DB.SetSystemConfig("dns_servers", strings.Join(cleanServers, ",")); err != nil { - return nil, GetDNSConfigOutput{}, err - } - if err := deps.DB.SetSystemConfig("dns_search", strings.TrimSpace(in.Search)); err != nil { - return nil, GetDNSConfigOutput{}, err - } - // Validate records. - if len(in.Records) > 64 { - return nil, GetDNSConfigOutput{}, errors.New("too many DNS records (max 64)") - } - records := make([]firewall.DNSRecord, 0, len(in.Records)) - for _, r := range in.Records { - r.Host = strings.TrimSpace(r.Host) - r.IP = strings.TrimSpace(r.IP) - if r.Host == "" || net.ParseIP(r.IP) == nil { - return nil, GetDNSConfigOutput{}, errors.New("invalid DNS record (host=" + r.Host + ", ip=" + r.IP + ")") - } - records = append(records, firewall.DNSRecord{Host: r.Host, IP: r.IP}) - } - if err := deps.DB.SetSystemConfig("dns_records", firewall.SerializeDNSRecords(records)); err != nil { - return nil, GetDNSConfigOutput{}, err - } - if deps.Firewall != nil { - _ = deps.Firewall.ForceRefresh() - } - cfg := firewall.LoadDNSConfig(deps.DB) - out := GetDNSConfigOutput{ - Servers: cfg.Servers, Search: cfg.Search, Defaults: cfg.Defaults, - Records: make([]DNSRecordView, 0, len(cfg.Records)), - } - for _, r := range cfg.Records { - out.Records = append(out.Records, DNSRecordView{Host: r.Host, IP: r.IP}) - } - return nil, out, nil - }, - ) - }) + regAddTool(rc, permAdmin, + &mcpsdk.Tool{ + Name: "set_dns_config", + Title: "Set DNS Config", + Description: "Update DNS settings. Servers must be literal IPs (not hostnames). Records (max 64) override DNS for specific hostnames. Idempotent — pass the desired full state.", + Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in SetDNSConfigInput) (*mcpsdk.CallToolResult, GetDNSConfigOutput, error) { + // Validate servers — must be IPs. + cleanServers := []string{} + for _, sIP := range in.Servers { + sIP = strings.TrimSpace(sIP) + if sIP == "" { + continue + } + if net.ParseIP(sIP) == nil { + return nil, GetDNSConfigOutput{}, errors.New("dns server must be a literal IP: " + sIP) + } + cleanServers = append(cleanServers, sIP) + } + if err := deps.DB.SetSystemConfig("dns_servers", strings.Join(cleanServers, ",")); err != nil { + return nil, GetDNSConfigOutput{}, err + } + if err := deps.DB.SetSystemConfig("dns_search", strings.TrimSpace(in.Search)); err != nil { + return nil, GetDNSConfigOutput{}, err + } + // Validate records. + if len(in.Records) > 64 { + return nil, GetDNSConfigOutput{}, errors.New("too many DNS records (max 64)") + } + records := make([]firewall.DNSRecord, 0, len(in.Records)) + for _, r := range in.Records { + r.Host = strings.TrimSpace(r.Host) + r.IP = strings.TrimSpace(r.IP) + if r.Host == "" || net.ParseIP(r.IP) == nil { + return nil, GetDNSConfigOutput{}, errors.New("invalid DNS record (host=" + r.Host + ", ip=" + r.IP + ")") + } + records = append(records, firewall.DNSRecord{Host: r.Host, IP: r.IP}) + } + if err := deps.DB.SetSystemConfig("dns_records", firewall.SerializeDNSRecords(records)); err != nil { + return nil, GetDNSConfigOutput{}, err + } + if deps.Firewall != nil { + _ = deps.Firewall.ForceRefresh() + } + cfg := firewall.LoadDNSConfig(deps.DB) + out := GetDNSConfigOutput{ + Servers: cfg.Servers, Search: cfg.Search, Defaults: cfg.Defaults, + Records: make([]DNSRecordView, 0, len(cfg.Records)), + } + for _, r := range cfg.Records { + out.Records = append(out.Records, DNSRecordView{Host: r.Host, IP: r.IP}) + } + return nil, out, nil + }, + ) } diff --git a/backend/internal/mcp/tools_fixtures.go b/backend/internal/mcp/tools_fixtures.go index bf73af6..6ade767 100644 --- a/backend/internal/mcp/tools_fixtures.go +++ b/backend/internal/mcp/tools_fixtures.go @@ -123,89 +123,86 @@ func normaliseFixtureInput(in *SaveFixtureInput) error { return nil } -func registerFixtureTools(s *mcpsdk.Server, deps Deps, perms permSet) { - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "list_fixtures", - Title: "List Fixtures", - Description: "List saved request fixtures for one function. Fixtures are reusable Postman-style presets (method, path, headers, body) used to invoke the function from the editor's Test pane or via test_function_with_fixture.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in ListFixturesInput) (*mcpsdk.CallToolResult, ListFixturesOutput, error) { - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, ListFixturesOutput{}, err - } - rows, err := deps.DB.ListFixtures(fn.ID) - if err != nil { - return nil, ListFixturesOutput{}, err - } - out := ListFixturesOutput{Fixtures: make([]FixtureView, 0, len(rows))} - for _, f := range rows { - out.Fixtures = append(out.Fixtures, toFixtureView(f)) - } - return nil, out, nil - }, - ) - }) - - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "save_fixture", - Title: "Save Fixture", - Description: "Create or update a saved request fixture for a function. Idempotent on (function_id, name) — re-calling with the same name overwrites method/path/headers/body. Use list_fixtures to see what's already saved before creating dupes.", - Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in SaveFixtureInput) (*mcpsdk.CallToolResult, SaveFixtureOutput, error) { - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, SaveFixtureOutput{}, err - } - if err := normaliseFixtureInput(&in); err != nil { - return nil, SaveFixtureOutput{}, err - } - headersJSON, _ := json.Marshal(in.Headers) - row := &database.Fixture{ - FunctionID: fn.ID, - Name: in.Name, - Method: in.Method, - Path: in.Path, - HeadersJSON: string(headersJSON), - Body: []byte(in.Body), - } - saved, err := deps.DB.UpsertFixture(row) - if err != nil { - return nil, SaveFixtureOutput{}, err - } - return nil, SaveFixtureOutput{Fixture: toFixtureView(saved)}, nil - }, - ) - }) - - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "delete_fixture", - Title: "Delete Fixture", - Description: "Remove a saved fixture by (function_id, name). Idempotent — returns ok even if the fixture didn't exist.", - Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in DeleteFixtureInput) (*mcpsdk.CallToolResult, DeleteFixtureOutput, error) { - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, DeleteFixtureOutput{}, err - } - name := strings.TrimSpace(in.Name) - if name == "" { - return nil, DeleteFixtureOutput{}, errors.New("name is required") - } - if err := deps.DB.DeleteFixture(fn.ID, name); err != nil { - return nil, DeleteFixtureOutput{}, err - } - return nil, DeleteFixtureOutput{Status: "deleted", Name: name}, nil - }, - ) - }) +func registerFixtureTools(rc *regCtx) { + deps := rc.deps + rc.group = "fixtures" + + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "list_fixtures", + Title: "List Fixtures", + Description: "List saved request fixtures for one function. Fixtures are reusable Postman-style presets (method, path, headers, body) used to invoke the function from the editor's Test pane or via test_function_with_fixture.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in ListFixturesInput) (*mcpsdk.CallToolResult, ListFixturesOutput, error) { + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, ListFixturesOutput{}, err + } + rows, err := deps.DB.ListFixtures(fn.ID) + if err != nil { + return nil, ListFixturesOutput{}, err + } + out := ListFixturesOutput{Fixtures: make([]FixtureView, 0, len(rows))} + for _, f := range rows { + out.Fixtures = append(out.Fixtures, toFixtureView(f)) + } + return nil, out, nil + }, + ) + + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "save_fixture", + Title: "Save Fixture", + Description: "Create or update a saved request fixture for a function. Idempotent on (function_id, name) — re-calling with the same name overwrites method/path/headers/body. Use list_fixtures to see what's already saved before creating dupes.", + Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in SaveFixtureInput) (*mcpsdk.CallToolResult, SaveFixtureOutput, error) { + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, SaveFixtureOutput{}, err + } + if err := normaliseFixtureInput(&in); err != nil { + return nil, SaveFixtureOutput{}, err + } + headersJSON, _ := json.Marshal(in.Headers) + row := &database.Fixture{ + FunctionID: fn.ID, + Name: in.Name, + Method: in.Method, + Path: in.Path, + HeadersJSON: string(headersJSON), + Body: []byte(in.Body), + } + saved, err := deps.DB.UpsertFixture(row) + if err != nil { + return nil, SaveFixtureOutput{}, err + } + return nil, SaveFixtureOutput{Fixture: toFixtureView(saved)}, nil + }, + ) + + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "delete_fixture", + Title: "Delete Fixture", + Description: "Remove a saved fixture by (function_id, name). Idempotent — returns ok even if the fixture didn't exist.", + Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in DeleteFixtureInput) (*mcpsdk.CallToolResult, DeleteFixtureOutput, error) { + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, DeleteFixtureOutput{}, err + } + name := strings.TrimSpace(in.Name) + if name == "" { + return nil, DeleteFixtureOutput{}, errors.New("name is required") + } + if err := deps.DB.DeleteFixture(fn.ID, name); err != nil { + return nil, DeleteFixtureOutput{}, err + } + return nil, DeleteFixtureOutput{Status: "deleted", Name: name}, nil + }, + ) } diff --git a/backend/internal/mcp/tools_functions.go b/backend/internal/mcp/tools_functions.go index 1500750..9f2c029 100644 --- a/backend/internal/mcp/tools_functions.go +++ b/backend/internal/mcp/tools_functions.go @@ -218,166 +218,157 @@ var userSettableStatuses = map[string]bool{"active": true, "inactive": true} // ─── registration ────────────────────────────────────────────────── -func registerFunctionTools(s *mcpsdk.Server, deps Deps, perms permSet) { - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "list_functions", - Title: "List Functions", - Description: "List all functions on this Orva instance. Each result includes invoke_url (fully-qualified canonical URL — call this directly, do NOT build it from parts) and routes (list of custom-route URLs, if any). Use id (a UUID) to refer to a function in other MCP tools, or name for human-friendly references. Supports pagination (limit/offset) and filtering by runtime, status, or substring search.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in ListFunctionsInput) (*mcpsdk.CallToolResult, ListFunctionsOutput, error) { - lim := in.Limit - if lim <= 0 { - lim = 50 - } - if lim > 200 { - lim = 200 - } - res, err := deps.DB.ListFunctions(database.ListFunctionsParams{ - Status: in.Status, Runtime: in.Runtime, Limit: lim, Offset: in.Offset, - }) - if err != nil { - return nil, ListFunctionsOutput{}, err - } - out := ListFunctionsOutput{Total: res.Total, Limit: lim, Offset: in.Offset} - q := strings.ToLower(strings.TrimSpace(in.Search)) - for _, fn := range res.Functions { - if q != "" { - if !strings.Contains(strings.ToLower(fn.Name), q) && !strings.Contains(strings.ToLower(fn.ID), q) { - continue - } +func registerFunctionTools(rc *regCtx) { + deps := rc.deps + rc.group = "functions" + + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "list_functions", + Title: "List Functions", + Description: "List all functions on this Orva instance. Each result includes invoke_url (fully-qualified canonical URL — call this directly, do NOT build it from parts) and routes (list of custom-route URLs, if any). Use id (a UUID) to refer to a function in other MCP tools, or name for human-friendly references. Supports pagination (limit/offset) and filtering by runtime, status, or substring search.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in ListFunctionsInput) (*mcpsdk.CallToolResult, ListFunctionsOutput, error) { + lim := in.Limit + if lim <= 0 { + lim = 50 + } + if lim > 200 { + lim = 200 + } + res, err := deps.DB.ListFunctions(database.ListFunctionsParams{ + Status: in.Status, Runtime: in.Runtime, Limit: lim, Offset: in.Offset, + }) + if err != nil { + return nil, ListFunctionsOutput{}, err + } + out := ListFunctionsOutput{Total: res.Total, Limit: lim, Offset: in.Offset} + q := strings.ToLower(strings.TrimSpace(in.Search)) + for _, fn := range res.Functions { + if q != "" { + if !strings.Contains(strings.ToLower(fn.Name), q) && !strings.Contains(strings.ToLower(fn.ID), q) { + continue } - out.Functions = append(out.Functions, toFunctionView(fn, deps)) - } - return nil, out, nil - }, - ) - }) - - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "get_function", - Title: "Get Function", - Description: "Fetch one function by id or name. Returns the full record including invoke_url (use verbatim to call the function over HTTP — never concatenate /fn/ + id manually), any custom routes, resource limits, env_vars, network_mode, auth_mode, and rate_limit_per_min.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in GetFunctionInput) (*mcpsdk.CallToolResult, FunctionView, error) { - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, FunctionView{}, err - } - return nil, toFunctionView(fn, deps), nil - }, - ) - }) - - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "get_function_source", - Title: "Get Function Source", - Description: "Read the deployed source code of a function (handler.py / handler.js plus requirements.txt or package.json if any). Useful for an agent that wants to inspect or modify what's running.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in GetFunctionSourceInput) (*mcpsdk.CallToolResult, GetFunctionSourceOutput, error) { - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, GetFunctionSourceOutput{}, err - } - out, err := readFunctionSource(deps, fn) - return nil, out, err - }, - ) - }) - - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "create_function", - Title: "Create Function", - Description: "BEFORE calling this: if you have not already called `get_orva_docs` in this conversation, call it first. Orva's handler contract, SDK shape, and event envelope diverge from Lambda / Vercel / Cloudflare Workers; agents that skip the docs and rely on training-data defaults consistently produce code that fails at first invoke (`require('orva')` not `import orva`, `kv.put` not `kv.set`, `exports.handler` not `export default`, etc.). Reading the docs once costs ~3 KB and prevents 4-6 round-trips of trial-and-error.\n\n" + - "Create a new function shell (no code yet). The function starts in `created` status — call deploy_function_inline next to ship code and have it activate. " + - "Most fields are REQUIRED so the function record carries explicit intent rather than silent defaults. Specifically you MUST provide: " + - "`name` (URL-safe identifier), " + - "`description` (one-sentence summary of what the function does — visible in list_functions and the dashboard), " + - "`runtime` (node22 / node24 / python313 / python314), " + - "`entrypoint` (handler file path; e.g. handler.js / handler.py / src/index.ts), " + - "`timeout_ms` (per-invocation cap; pick from your handler's expected work — fast CRUD ~5000-10000, AI/LLM ~30000-60000, heavy reports 120000+), " + - "`memory_mb` (RAM cap; tiny handlers 64, with frameworks 128-256, image/PDF/ML 512+), " + - "`cpus` (CPU shares; IO-bound 0.25-0.5, mixed 0.5-1, CPU-bound 1+), " + - "`network_mode` ('egress' if the handler imports `orva` / makes external HTTPS, 'none' only for pure compute), " + - "`auth_mode` ('platform_key' / 'signed' for anything that handles user data; 'none' only for genuinely public endpoints). " + - "Optional: env_vars, max_concurrency, concurrency_policy, rate_limit_per_min — sane defaults. " + - "Defaulting any of the required fields silently has been a frequent source of bugs (auth=none on private endpoints, network_mode=none on SDK handlers, undersized memory) — being explicit costs ~10 extra lines and prevents the whole class of foot-gun.", - Annotations: &mcpsdk.ToolAnnotations{ - DestructiveHint: ptrFalse(), - OpenWorldHint: ptrFalse(), - }, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in CreateFunctionInput) (*mcpsdk.CallToolResult, FunctionView, error) { - fn, err := createFunction(deps, in) - if err != nil { - return nil, FunctionView{}, err } - return nil, toFunctionView(fn, deps), nil - }, - ) - }) - - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "update_function", - Title: "Update Function", - Description: "BEFORE calling this with code-shape implications (entrypoint changes, runtime swaps, etc.): if you have not already called `get_orva_docs` in this conversation, call it first. Orva's handler contract diverges from Lambda / Vercel / Workers and skipping the docs is the leading cause of update_function calls that activate broken handlers.\n\n" + - "Patch a function's settings. Any field omitted is left unchanged. Flipping memory/cpus/env_vars/network_mode drains the warm pool so the next invocation respawns with the new config.", - Annotations: &mcpsdk.ToolAnnotations{ - IdempotentHint: true, - OpenWorldHint: ptrFalse(), - }, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in UpdateFunctionInput) (*mcpsdk.CallToolResult, FunctionView, error) { - fn, err := updateFunction(deps, in) - if err != nil { - return nil, FunctionView{}, err - } - return nil, toFunctionView(fn, deps), nil + out.Functions = append(out.Functions, toFunctionView(fn, deps)) + } + return nil, out, nil + }, + ) + + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "get_function", + Title: "Get Function", + Description: "Fetch one function by id or name. Returns the full record including invoke_url (use verbatim to call the function over HTTP — never concatenate /fn/ + id manually), any custom routes, resource limits, env_vars, network_mode, auth_mode, and rate_limit_per_min.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in GetFunctionInput) (*mcpsdk.CallToolResult, FunctionView, error) { + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, FunctionView{}, err + } + return nil, toFunctionView(fn, deps), nil + }, + ) + + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "get_function_source", + Title: "Get Function Source", + Description: "Read the deployed source code of a function (handler.py / handler.js plus requirements.txt or package.json if any). Useful for an agent that wants to inspect or modify what's running.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in GetFunctionSourceInput) (*mcpsdk.CallToolResult, GetFunctionSourceOutput, error) { + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, GetFunctionSourceOutput{}, err + } + out, err := readFunctionSource(deps, fn) + return nil, out, err + }, + ) + + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "create_function", + Title: "Create Function", + Description: "BEFORE calling this: if you have not already called `get_orva_docs` in this conversation, call it first. Orva's handler contract, SDK shape, and event envelope diverge from Lambda / Vercel / Cloudflare Workers; agents that skip the docs and rely on training-data defaults consistently produce code that fails at first invoke (`require('orva')` not `import orva`, `kv.put` not `kv.set`, `exports.handler` not `export default`, etc.). Reading the docs once costs ~3 KB and prevents 4-6 round-trips of trial-and-error.\n\n" + + "Create a new function shell (no code yet). The function starts in `created` status — call deploy_function_inline next to ship code and have it activate. " + + "Most fields are REQUIRED so the function record carries explicit intent rather than silent defaults. Specifically you MUST provide: " + + "`name` (URL-safe identifier), " + + "`description` (one-sentence summary of what the function does — visible in list_functions and the dashboard), " + + "`runtime` (node22 / node24 / python313 / python314), " + + "`entrypoint` (handler file path; e.g. handler.js / handler.py / src/index.ts), " + + "`timeout_ms` (per-invocation cap; pick from your handler's expected work — fast CRUD ~5000-10000, AI/LLM ~30000-60000, heavy reports 120000+), " + + "`memory_mb` (RAM cap; tiny handlers 64, with frameworks 128-256, image/PDF/ML 512+), " + + "`cpus` (CPU shares; IO-bound 0.25-0.5, mixed 0.5-1, CPU-bound 1+), " + + "`network_mode` ('egress' if the handler imports `orva` / makes external HTTPS, 'none' only for pure compute), " + + "`auth_mode` ('platform_key' / 'signed' for anything that handles user data; 'none' only for genuinely public endpoints). " + + "Optional: env_vars, max_concurrency, concurrency_policy, rate_limit_per_min — sane defaults. " + + "Defaulting any of the required fields silently has been a frequent source of bugs (auth=none on private endpoints, network_mode=none on SDK handlers, undersized memory) — being explicit costs ~10 extra lines and prevents the whole class of foot-gun.", + Annotations: &mcpsdk.ToolAnnotations{ + DestructiveHint: ptrFalse(), + OpenWorldHint: ptrFalse(), }, - ) - }) - - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "delete_function", - Title: "Delete Function", - Description: "Permanently delete a function and all its deployments, secrets, routes, and execution history. Pass confirm=true. Irreversible — only do this when the user has explicitly asked.", - Annotations: &mcpsdk.ToolAnnotations{ - DestructiveHint: ptrTrue(), - OpenWorldHint: ptrFalse(), - }, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in CreateFunctionInput) (*mcpsdk.CallToolResult, FunctionView, error) { + fn, err := createFunction(deps, in) + if err != nil { + return nil, FunctionView{}, err + } + return nil, toFunctionView(fn, deps), nil + }, + ) + + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "update_function", + Title: "Update Function", + Description: "BEFORE calling this with code-shape implications (entrypoint changes, runtime swaps, etc.): if you have not already called `get_orva_docs` in this conversation, call it first. Orva's handler contract diverges from Lambda / Vercel / Workers and skipping the docs is the leading cause of update_function calls that activate broken handlers.\n\n" + + "Patch a function's settings. Any field omitted is left unchanged. Flipping memory/cpus/env_vars/network_mode drains the warm pool so the next invocation respawns with the new config.", + Annotations: &mcpsdk.ToolAnnotations{ + IdempotentHint: true, + OpenWorldHint: ptrFalse(), }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in DeleteFunctionInput) (*mcpsdk.CallToolResult, DeletedOutput, error) { - if !in.Confirm { - return nil, DeletedOutput{}, errors.New("delete refused: pass confirm=true to acknowledge irreversibility") - } - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, DeletedOutput{}, err - } - if err := deps.Registry.Delete(fn.ID); err != nil { - return nil, DeletedOutput{}, err - } - if deps.PoolMgr != nil { - deps.PoolMgr.DrainAndRemove(fn.ID) - } - return nil, DeletedOutput{DeletedID: fn.ID}, nil + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in UpdateFunctionInput) (*mcpsdk.CallToolResult, FunctionView, error) { + fn, err := updateFunction(deps, in) + if err != nil { + return nil, FunctionView{}, err + } + return nil, toFunctionView(fn, deps), nil + }, + ) + + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "delete_function", + Title: "Delete Function", + Description: "Permanently delete a function and all its deployments, secrets, routes, and execution history. Pass confirm=true. Irreversible — only do this when the user has explicitly asked.", + Annotations: &mcpsdk.ToolAnnotations{ + DestructiveHint: ptrTrue(), + OpenWorldHint: ptrFalse(), }, - ) - }) + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in DeleteFunctionInput) (*mcpsdk.CallToolResult, DeletedOutput, error) { + if !in.Confirm { + return nil, DeletedOutput{}, errors.New("delete refused: pass confirm=true to acknowledge irreversibility") + } + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, DeletedOutput{}, err + } + if err := deps.Registry.Delete(fn.ID); err != nil { + return nil, DeletedOutput{}, err + } + if deps.PoolMgr != nil { + deps.PoolMgr.DrainAndRemove(fn.ID) + } + return nil, DeletedOutput{DeletedID: fn.ID}, nil + }, + ) } // ─── helpers shared with deploy tools ────────────────────────────── diff --git a/backend/internal/mcp/tools_inbound_webhooks.go b/backend/internal/mcp/tools_inbound_webhooks.go index bb32b89..ad446db 100644 --- a/backend/internal/mcp/tools_inbound_webhooks.go +++ b/backend/internal/mcp/tools_inbound_webhooks.go @@ -77,109 +77,105 @@ type InboundWebhookOpOutput struct { Status string `json:"status,omitempty"` } -func registerInboundWebhookTools(s *mcpsdk.Server, deps Deps, perms permSet) { - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "list_inbound_webhooks", - Title: "List Inbound Webhooks", - Description: "List inbound webhook triggers configured for a function. Each row exposes its trigger URL (/webhook/) and signature format. Plaintext secrets are NEVER returned — use create_inbound_webhook to mint a new one if you've lost it.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in ListInboundWebhooksInput) (*mcpsdk.CallToolResult, ListInboundWebhooksOutput, error) { - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, ListInboundWebhooksOutput{}, err - } - rows, err := deps.DB.ListInboundWebhooksForFunction(fn.ID) - if err != nil { - return nil, ListInboundWebhooksOutput{}, err - } - out := ListInboundWebhooksOutput{InboundWebhooks: make([]InboundWebhookView, 0, len(rows))} - for _, r := range rows { - r.FunctionName = fn.Name - out.InboundWebhooks = append(out.InboundWebhooks, toInboundWebhookView(r)) - } - return nil, out, nil - }, - ) - }) +func registerInboundWebhookTools(rc *regCtx) { + deps := rc.deps + rc.group = "inbound_webhooks" + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "list_inbound_webhooks", + Title: "List Inbound Webhooks", + Description: "List inbound webhook triggers configured for a function. Each row exposes its trigger URL (/webhook/) and signature format. Plaintext secrets are NEVER returned — use create_inbound_webhook to mint a new one if you've lost it.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in ListInboundWebhooksInput) (*mcpsdk.CallToolResult, ListInboundWebhooksOutput, error) { + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, ListInboundWebhooksOutput{}, err + } + rows, err := deps.DB.ListInboundWebhooksForFunction(fn.ID) + if err != nil { + return nil, ListInboundWebhooksOutput{}, err + } + out := ListInboundWebhooksOutput{InboundWebhooks: make([]InboundWebhookView, 0, len(rows))} + for _, r := range rows { + r.FunctionName = fn.Name + out.InboundWebhooks = append(out.InboundWebhooks, toInboundWebhookView(r)) + } + return nil, out, nil + }, + ) - gatedAdd(perms, permAdmin, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "create_inbound_webhook", - Title: "Create Inbound Webhook", - Description: "Create a new inbound webhook trigger for a function. Returns the trigger URL AND the plaintext HMAC secret — the secret is shown ONLY once; capture it now and configure your upstream service (GitHub, Stripe, Slack, your own backend) to sign request bodies with it.", - Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: false, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in CreateInboundWebhookInput) (*mcpsdk.CallToolResult, CreateInboundWebhookOutput, error) { - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, CreateInboundWebhookOutput{}, err - } - name := strings.TrimSpace(in.Name) - if name == "" { - return nil, CreateInboundWebhookOutput{}, errors.New("name is required") - } - format := strings.TrimSpace(in.SignatureFormat) - if format == "" { - format = "hmac_sha256_hex" - } - if _, ok := database.AllowedInboundFormats[format]; !ok { - return nil, CreateInboundWebhookOutput{}, errors.New("unknown signature_format: must be hmac_sha256_hex, hmac_sha256_base64, github, stripe, or slack") - } - header := strings.TrimSpace(in.SignatureHeader) - if header == "" { - header = database.DefaultSignatureHeader(format) - } - secret := database.NewInboundWebhookSecret() - row := &database.InboundWebhook{ - FunctionID: fn.ID, - Name: name, - Secret: secret, - SignatureHeader: header, - SignatureFormat: format, - Active: true, - } - if err := deps.DB.InsertInboundWebhook(row); err != nil { - return nil, CreateInboundWebhookOutput{}, err - } - row.SecretPreview = secret[:8] + "…" - row.FunctionName = fn.Name - return nil, CreateInboundWebhookOutput{ - InboundWebhook: toInboundWebhookView(row), - Secret: secret, - }, nil - }, - ) - }) + regAddTool(rc, permAdmin, + &mcpsdk.Tool{ + Name: "create_inbound_webhook", + Title: "Create Inbound Webhook", + Description: "Create a new inbound webhook trigger for a function. Returns the trigger URL AND the plaintext HMAC secret — the secret is shown ONLY once; capture it now and configure your upstream service (GitHub, Stripe, Slack, your own backend) to sign request bodies with it.", + Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: false, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in CreateInboundWebhookInput) (*mcpsdk.CallToolResult, CreateInboundWebhookOutput, error) { + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, CreateInboundWebhookOutput{}, err + } + name := strings.TrimSpace(in.Name) + if name == "" { + return nil, CreateInboundWebhookOutput{}, errors.New("name is required") + } + format := strings.TrimSpace(in.SignatureFormat) + if format == "" { + format = "hmac_sha256_hex" + } + if _, ok := database.AllowedInboundFormats[format]; !ok { + return nil, CreateInboundWebhookOutput{}, errors.New("unknown signature_format: must be hmac_sha256_hex, hmac_sha256_base64, github, stripe, or slack") + } + header := strings.TrimSpace(in.SignatureHeader) + if header == "" { + header = database.DefaultSignatureHeader(format) + } + secret := database.NewInboundWebhookSecret() + row := &database.InboundWebhook{ + FunctionID: fn.ID, + Name: name, + Secret: secret, + SignatureHeader: header, + SignatureFormat: format, + Active: true, + } + if err := deps.DB.InsertInboundWebhook(row); err != nil { + return nil, CreateInboundWebhookOutput{}, err + } + row.SecretPreview = secret[:8] + "…" + row.FunctionName = fn.Name + return nil, CreateInboundWebhookOutput{ + InboundWebhook: toInboundWebhookView(row), + Secret: secret, + }, nil + }, + ) - gatedAdd(perms, permAdmin, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "delete_inbound_webhook", - Title: "Delete Inbound Webhook", - Description: "Remove an inbound webhook trigger. Pass confirm=true. The trigger URL stops accepting calls immediately; previously-signed requests will return 404.", - Annotations: &mcpsdk.ToolAnnotations{DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in DeleteInboundWebhookInput) (*mcpsdk.CallToolResult, InboundWebhookOpOutput, error) { - if !in.Confirm { - return nil, InboundWebhookOpOutput{}, errors.New("delete refused: pass confirm=true") - } - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, InboundWebhookOpOutput{}, err - } - row, err := deps.DB.GetInboundWebhook(in.ID) - if err != nil || row.FunctionID != fn.ID { - return nil, InboundWebhookOpOutput{}, errors.New("inbound webhook not found") - } - if err := deps.DB.DeleteInboundWebhook(in.ID); err != nil { - return nil, InboundWebhookOpOutput{}, err - } - return nil, InboundWebhookOpOutput{ID: in.ID, Status: "deleted"}, nil - }, - ) - }) + regAddTool(rc, permAdmin, + &mcpsdk.Tool{ + Name: "delete_inbound_webhook", + Title: "Delete Inbound Webhook", + Description: "Remove an inbound webhook trigger. Pass confirm=true. The trigger URL stops accepting calls immediately; previously-signed requests will return 404.", + Annotations: &mcpsdk.ToolAnnotations{DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in DeleteInboundWebhookInput) (*mcpsdk.CallToolResult, InboundWebhookOpOutput, error) { + if !in.Confirm { + return nil, InboundWebhookOpOutput{}, errors.New("delete refused: pass confirm=true") + } + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, InboundWebhookOpOutput{}, err + } + row, err := deps.DB.GetInboundWebhook(in.ID) + if err != nil || row.FunctionID != fn.ID { + return nil, InboundWebhookOpOutput{}, errors.New("inbound webhook not found") + } + if err := deps.DB.DeleteInboundWebhook(in.ID); err != nil { + return nil, InboundWebhookOpOutput{}, err + } + return nil, InboundWebhookOpOutput{ID: in.ID, Status: "deleted"}, nil + }, + ) } diff --git a/backend/internal/mcp/tools_invoke.go b/backend/internal/mcp/tools_invoke.go index 7fb9527..59346d4 100644 --- a/backend/internal/mcp/tools_invoke.go +++ b/backend/internal/mcp/tools_invoke.go @@ -12,9 +12,9 @@ import ( "time" "github.com/Harsh-2002/Orva/backend/internal/database" - "github.com/Harsh-2002/Orva/internal/ids" "github.com/Harsh-2002/Orva/backend/internal/sandbox" "github.com/Harsh-2002/Orva/backend/internal/trace" + "github.com/Harsh-2002/Orva/internal/ids" mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" ) @@ -174,163 +174,152 @@ type TestFunctionWithFixtureOutput struct { // ─── registration ────────────────────────────────────────────────── -func registerInvokeTools(s *mcpsdk.Server, deps Deps, perms permSet) { - gatedAdd(perms, permInvoke, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "invoke_function", - Title: "Invoke Function", - Description: "Call a function and return its response. `method` is REQUIRED — pick GET for read endpoints, POST for create/write, PUT/PATCH for updates, DELETE for removals (no silent default; an invocation that uses the wrong verb usually returns 404/405 which is hard to debug). Pass `body` as either an object (sent as JSON) or a string. Returns status_code, headers, body, plus execution_id you can pass to get_execution_logs if you want stderr. Bypasses the function's auth_mode (the agent's MCP bearer is already trusted). When the handler crashes with a network-shaped error (ENETUNREACH / fetch failed / OrvaUnavailableError) on a function with network_mode=none, the response includes an `orva_hint` telling you exactly what to fix.", - Annotations: &mcpsdk.ToolAnnotations{ - DestructiveHint: ptrFalse(), - OpenWorldHint: ptrTrue(), // function may call external APIs - }, - }, - func(ctx context.Context, _ *mcpsdk.CallToolRequest, in InvokeFunctionInput) (*mcpsdk.CallToolResult, InvokeFunctionOutput, error) { - return invokeFunction(ctx, deps, in) - }, - ) - }) - - gatedAdd(perms, permInvoke, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "test_function_with_fixture", - Title: "Test Function With Fixture", - Description: "Invoke a function using a previously-saved fixture as the request envelope. Applies optional shallow-merge overrides (override wins on key collision). Returns the same shape as invoke_function plus a `fixture` block listing which overrides were applied. Use list_fixtures to see what's saved.", - Annotations: &mcpsdk.ToolAnnotations{ - DestructiveHint: ptrFalse(), - OpenWorldHint: ptrTrue(), - }, +func registerInvokeTools(rc *regCtx) { + deps := rc.deps + rc.group = "invoke" + + regAddTool(rc, permInvoke, + &mcpsdk.Tool{ + Name: "invoke_function", + Title: "Invoke Function", + Description: "Call a function and return its response. `method` is REQUIRED — pick GET for read endpoints, POST for create/write, PUT/PATCH for updates, DELETE for removals (no silent default; an invocation that uses the wrong verb usually returns 404/405 which is hard to debug). Pass `body` as either an object (sent as JSON) or a string. Returns status_code, headers, body, plus execution_id you can pass to get_execution_logs if you want stderr. Bypasses the function's auth_mode (the agent's MCP bearer is already trusted). When the handler crashes with a network-shaped error (ENETUNREACH / fetch failed / OrvaUnavailableError) on a function with network_mode=none, the response includes an `orva_hint` telling you exactly what to fix.", + Annotations: &mcpsdk.ToolAnnotations{ + DestructiveHint: ptrFalse(), + OpenWorldHint: ptrTrue(), // function may call external APIs }, - func(ctx context.Context, _ *mcpsdk.CallToolRequest, in TestFunctionWithFixtureInput) (*mcpsdk.CallToolResult, TestFunctionWithFixtureOutput, error) { - return testFunctionWithFixture(ctx, deps, in) - }, - ) - }) - - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "list_executions", - Title: "List Executions", - Description: "List recent invocations across all functions or filtered to one. Useful for an agent debugging why a function is failing — combine with get_execution_logs to see stderr.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(ctx context.Context, _ *mcpsdk.CallToolRequest, in InvokeFunctionInput) (*mcpsdk.CallToolResult, InvokeFunctionOutput, error) { + return invokeFunction(ctx, deps, in) + }, + ) + + regAddTool(rc, permInvoke, + &mcpsdk.Tool{ + Name: "test_function_with_fixture", + Title: "Test Function With Fixture", + Description: "Invoke a function using a previously-saved fixture as the request envelope. Applies optional shallow-merge overrides (override wins on key collision). Returns the same shape as invoke_function plus a `fixture` block listing which overrides were applied. Use list_fixtures to see what's saved.", + Annotations: &mcpsdk.ToolAnnotations{ + DestructiveHint: ptrFalse(), + OpenWorldHint: ptrTrue(), }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in ListExecutionsInput) (*mcpsdk.CallToolResult, ListExecutionsOutput, error) { - params := database.ListExecutionsParams{ - Status: in.Status, Since: in.Since, Until: in.Until, - Search: in.Search, Limit: in.Limit, Offset: in.Offset, - } - if in.FunctionID != "" { - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, ListExecutionsOutput{}, err - } - params.FunctionID = fn.ID - } - if params.Limit <= 0 { - params.Limit = 50 - } - if params.Limit > 200 { - params.Limit = 200 - } - res, err := deps.DB.ListExecutions(params) + }, + func(ctx context.Context, _ *mcpsdk.CallToolRequest, in TestFunctionWithFixtureInput) (*mcpsdk.CallToolResult, TestFunctionWithFixtureOutput, error) { + return testFunctionWithFixture(ctx, deps, in) + }, + ) + + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "list_executions", + Title: "List Executions", + Description: "List recent invocations across all functions or filtered to one. Useful for an agent debugging why a function is failing — combine with get_execution_logs to see stderr.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in ListExecutionsInput) (*mcpsdk.CallToolResult, ListExecutionsOutput, error) { + params := database.ListExecutionsParams{ + Status: in.Status, Since: in.Since, Until: in.Until, + Search: in.Search, Limit: in.Limit, Offset: in.Offset, + } + if in.FunctionID != "" { + fn, err := resolveFunction(deps, in.FunctionID) if err != nil { return nil, ListExecutionsOutput{}, err } - out := ListExecutionsOutput{Total: res.Total, Limit: params.Limit, Offset: params.Offset} - for _, e := range res.Executions { - out.Executions = append(out.Executions, toExecutionView(e)) - } - return nil, out, nil - }, - ) - }) - - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "get_execution", - Title: "Get Execution", - Description: "Fetch one execution by id. Returns status, status_code, duration_ms, cold_start, and any error_message.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in GetExecutionInput) (*mcpsdk.CallToolResult, ExecutionView, error) { - e, err := deps.DB.GetExecution(in.ExecutionID) - if err != nil { - return nil, ExecutionView{}, fmt.Errorf("execution not found: %s", in.ExecutionID) - } - return nil, toExecutionView(e), nil - }, - ) - }) - - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "get_execution_logs", - Title: "Get Execution Logs", - Description: "Read the captured stderr from one execution. Stdout was already returned as the response body to whoever invoked. Returns empty string if the function logged nothing to stderr.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in GetExecutionInput) (*mcpsdk.CallToolResult, GetExecutionLogsOutput, error) { - log, err := deps.DB.GetExecutionLog(in.ExecutionID) - if err != nil { - return nil, GetExecutionLogsOutput{ExecutionID: in.ExecutionID}, nil - } - return nil, GetExecutionLogsOutput{ExecutionID: log.ExecutionID, Stderr: log.Stderr}, nil - }, - ) - }) - - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "delete_execution", - Title: "Delete Execution", - Description: "Permanently delete one execution row and its captured stderr. Pass confirm=true.", - Annotations: &mcpsdk.ToolAnnotations{DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in DeleteExecutionInput) (*mcpsdk.CallToolResult, DeletedOutput, error) { - if !in.Confirm { - return nil, DeletedOutput{}, errors.New("delete refused: pass confirm=true") - } - if err := deps.DB.DeleteExecution(in.ExecutionID); err != nil { - return nil, DeletedOutput{}, err - } - return nil, DeletedOutput{DeletedID: in.ExecutionID}, nil - }, - ) - }) - - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "bulk_delete_executions", - Title: "Bulk Delete Executions", - Description: "Delete multiple execution rows. Max 1000 ids per call. Pass confirm=true. Returns counts of deleted vs failed.", - Annotations: &mcpsdk.ToolAnnotations{DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in BulkDeleteExecutionsInput) (*mcpsdk.CallToolResult, BulkDeleteOutput, error) { - if !in.Confirm { - return nil, BulkDeleteOutput{}, errors.New("delete refused: pass confirm=true") - } - if len(in.IDs) > 1000 { - return nil, BulkDeleteOutput{}, errors.New("too many ids (max 1000 per call)") - } - out := BulkDeleteOutput{} - for _, id := range in.IDs { - if err := deps.DB.DeleteExecution(id); err != nil { - out.Failed++ - } else { - out.Deleted++ - } + params.FunctionID = fn.ID + } + if params.Limit <= 0 { + params.Limit = 50 + } + if params.Limit > 200 { + params.Limit = 200 + } + res, err := deps.DB.ListExecutions(params) + if err != nil { + return nil, ListExecutionsOutput{}, err + } + out := ListExecutionsOutput{Total: res.Total, Limit: params.Limit, Offset: params.Offset} + for _, e := range res.Executions { + out.Executions = append(out.Executions, toExecutionView(e)) + } + return nil, out, nil + }, + ) + + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "get_execution", + Title: "Get Execution", + Description: "Fetch one execution by id. Returns status, status_code, duration_ms, cold_start, and any error_message.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in GetExecutionInput) (*mcpsdk.CallToolResult, ExecutionView, error) { + e, err := deps.DB.GetExecution(in.ExecutionID) + if err != nil { + return nil, ExecutionView{}, fmt.Errorf("execution not found: %s", in.ExecutionID) + } + return nil, toExecutionView(e), nil + }, + ) + + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "get_execution_logs", + Title: "Get Execution Logs", + Description: "Read the captured stderr from one execution. Stdout was already returned as the response body to whoever invoked. Returns empty string if the function logged nothing to stderr.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in GetExecutionInput) (*mcpsdk.CallToolResult, GetExecutionLogsOutput, error) { + log, err := deps.DB.GetExecutionLog(in.ExecutionID) + if err != nil { + return nil, GetExecutionLogsOutput{ExecutionID: in.ExecutionID}, nil + } + return nil, GetExecutionLogsOutput{ExecutionID: log.ExecutionID, Stderr: log.Stderr}, nil + }, + ) + + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "delete_execution", + Title: "Delete Execution", + Description: "Permanently delete one execution row and its captured stderr. Pass confirm=true.", + Annotations: &mcpsdk.ToolAnnotations{DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in DeleteExecutionInput) (*mcpsdk.CallToolResult, DeletedOutput, error) { + if !in.Confirm { + return nil, DeletedOutput{}, errors.New("delete refused: pass confirm=true") + } + if err := deps.DB.DeleteExecution(in.ExecutionID); err != nil { + return nil, DeletedOutput{}, err + } + return nil, DeletedOutput{DeletedID: in.ExecutionID}, nil + }, + ) + + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "bulk_delete_executions", + Title: "Bulk Delete Executions", + Description: "Delete multiple execution rows. Max 1000 ids per call. Pass confirm=true. Returns counts of deleted vs failed.", + Annotations: &mcpsdk.ToolAnnotations{DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in BulkDeleteExecutionsInput) (*mcpsdk.CallToolResult, BulkDeleteOutput, error) { + if !in.Confirm { + return nil, BulkDeleteOutput{}, errors.New("delete refused: pass confirm=true") + } + if len(in.IDs) > 1000 { + return nil, BulkDeleteOutput{}, errors.New("too many ids (max 1000 per call)") + } + out := BulkDeleteOutput{} + for _, id := range in.IDs { + if err := deps.DB.DeleteExecution(id); err != nil { + out.Failed++ + } else { + out.Deleted++ } - return nil, out, nil - }, - ) - }) + } + return nil, out, nil + }, + ) } // ─── invoke implementation ───────────────────────────────────────── @@ -394,8 +383,8 @@ func invokeFunction(ctx context.Context, deps Deps, in InvokeFunctionInput) (*mc // auto-Content-Type below picks JSON for the json branch and leaves // the caller's Headers untouched for the string branch. var ( - bodyStr string - autoCT string + bodyStr string + autoCT string ) if in.Body != nil { switch in.Body.Type { diff --git a/backend/internal/mcp/tools_jobs.go b/backend/internal/mcp/tools_jobs.go index 7bc717e..63be0af 100644 --- a/backend/internal/mcp/tools_jobs.go +++ b/backend/internal/mcp/tools_jobs.go @@ -18,17 +18,17 @@ type JobView struct { ID string `json:"id"` FunctionID string `json:"function_id"` FunctionName string `json:"function_name,omitempty"` - Status string `json:"status"` + Status string `json:"status"` // See tools_cron.go for the rationale on map[string]any over `any`. - Payload map[string]any `json:"payload"` - ScheduledAt string `json:"scheduled_at"` - StartedAt string `json:"started_at,omitempty"` - FinishedAt string `json:"finished_at,omitempty"` - Attempts int `json:"attempts"` - MaxAttempts int `json:"max_attempts"` - LastError string `json:"last_error,omitempty"` - CreatedAt string `json:"created_at"` - Replayed bool `json:"replayed,omitempty" jsonschema:"true when an idempotent enqueue matched an existing row instead of creating a new one"` + Payload map[string]any `json:"payload"` + ScheduledAt string `json:"scheduled_at"` + StartedAt string `json:"started_at,omitempty"` + FinishedAt string `json:"finished_at,omitempty"` + Attempts int `json:"attempts"` + MaxAttempts int `json:"max_attempts"` + LastError string `json:"last_error,omitempty"` + CreatedAt string `json:"created_at"` + Replayed bool `json:"replayed,omitempty" jsonschema:"true when an idempotent enqueue matched an existing row instead of creating a new one"` } func toJobView(j *database.Job) JobView { @@ -98,139 +98,132 @@ type JobOpOutput struct { Status string `json:"status,omitempty"` } -func registerJobTools(s *mcpsdk.Server, deps Deps, perms permSet) { - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "enqueue_job", - Title: "Enqueue Job", - Description: "Enqueue a background job. Returns immediately with the job id; the scheduler will dispatch the function within ~5s (or at scheduled_at if supplied). Failed runs retry with exponential backoff up to max_attempts.", - Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: false, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in EnqueueJobInput) (*mcpsdk.CallToolResult, JobView, error) { - fn, err := resolveFunction(deps, in.FunctionID) +func registerJobTools(rc *regCtx) { + deps := rc.deps + rc.group = "jobs" + + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "enqueue_job", + Title: "Enqueue Job", + Description: "Enqueue a background job. Returns immediately with the job id; the scheduler will dispatch the function within ~5s (or at scheduled_at if supplied). Failed runs retry with exponential backoff up to max_attempts.", + Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: false, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in EnqueueJobInput) (*mcpsdk.CallToolResult, JobView, error) { + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, JobView{}, err + } + payload := []byte("{}") + if in.Payload != nil { + b, err := json.Marshal(in.Payload) if err != nil { - return nil, JobView{}, err - } - payload := []byte("{}") - if in.Payload != nil { - b, err := json.Marshal(in.Payload) - if err != nil { - return nil, JobView{}, errors.New("payload must be JSON-serializable") - } - payload = b + return nil, JobView{}, errors.New("payload must be JSON-serializable") } - job := &database.Job{ - FunctionID: fn.ID, - Payload: payload, - MaxAttempts: in.MaxAttempts, - IdempotencyKey: in.IdempotencyKey, - IdempotencyWindowSeconds: in.IdempotencyWindowSeconds, - } - if in.ScheduledAt != "" { - t, err := time.Parse(time.RFC3339, in.ScheduledAt) - if err != nil { - return nil, JobView{}, errors.New("scheduled_at must be RFC3339") - } - job.ScheduledAt = t.UTC() - } - if err := deps.DB.EnqueueJob(job); err != nil { - return nil, JobView{}, err + payload = b + } + job := &database.Job{ + FunctionID: fn.ID, + Payload: payload, + MaxAttempts: in.MaxAttempts, + IdempotencyKey: in.IdempotencyKey, + IdempotencyWindowSeconds: in.IdempotencyWindowSeconds, + } + if in.ScheduledAt != "" { + t, err := time.Parse(time.RFC3339, in.ScheduledAt) + if err != nil { + return nil, JobView{}, errors.New("scheduled_at must be RFC3339") } - job.FunctionName = fn.Name - return nil, toJobView(job), nil - }, - ) - }) + job.ScheduledAt = t.UTC() + } + if err := deps.DB.EnqueueJob(job); err != nil { + return nil, JobView{}, err + } + job.FunctionName = fn.Name + return nil, toJobView(job), nil + }, + ) - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "list_jobs", - Title: "List Jobs", - Description: "List background jobs, optionally filtered by status or function. Useful for inspecting the queue, surfacing failures, or auditing recent activity.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in ListJobsInput) (*mcpsdk.CallToolResult, ListJobsOutput, error) { - fnID := "" - if strings.TrimSpace(in.FunctionID) != "" { - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, ListJobsOutput{}, err - } - fnID = fn.ID - } - rows, err := deps.DB.ListJobs(in.Status, fnID, in.Limit) + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "list_jobs", + Title: "List Jobs", + Description: "List background jobs, optionally filtered by status or function. Useful for inspecting the queue, surfacing failures, or auditing recent activity.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in ListJobsInput) (*mcpsdk.CallToolResult, ListJobsOutput, error) { + fnID := "" + if strings.TrimSpace(in.FunctionID) != "" { + fn, err := resolveFunction(deps, in.FunctionID) if err != nil { return nil, ListJobsOutput{}, err } - out := ListJobsOutput{Jobs: make([]JobView, 0, len(rows))} - for _, j := range rows { - out.Jobs = append(out.Jobs, toJobView(j)) - } - return nil, out, nil - }, - ) - }) + fnID = fn.ID + } + rows, err := deps.DB.ListJobs(in.Status, fnID, in.Limit) + if err != nil { + return nil, ListJobsOutput{}, err + } + out := ListJobsOutput{Jobs: make([]JobView, 0, len(rows))} + for _, j := range rows { + out.Jobs = append(out.Jobs, toJobView(j)) + } + return nil, out, nil + }, + ) - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "get_job", - Title: "Get Job", - Description: "Fetch a single job by id. Includes the original payload, full retry history, and the most recent error.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in JobIDInput) (*mcpsdk.CallToolResult, JobView, error) { - j, err := deps.DB.GetJob(in.ID) - if err != nil { - return nil, JobView{}, errors.New("job not found") - } - if fn, err := deps.DB.GetFunction(j.FunctionID); err == nil { - j.FunctionName = fn.Name - } - return nil, toJobView(j), nil - }, - ) - }) + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "get_job", + Title: "Get Job", + Description: "Fetch a single job by id. Includes the original payload, full retry history, and the most recent error.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in JobIDInput) (*mcpsdk.CallToolResult, JobView, error) { + j, err := deps.DB.GetJob(in.ID) + if err != nil { + return nil, JobView{}, errors.New("job not found") + } + if fn, err := deps.DB.GetFunction(j.FunctionID); err == nil { + j.FunctionName = fn.Name + } + return nil, toJobView(j), nil + }, + ) - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "retry_job", - Title: "Retry Job", - Description: "Reset a terminal job (status=failed) back to pending so the scheduler picks it up on the next tick. attempts is reset to 0.", - Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in JobIDInput) (*mcpsdk.CallToolResult, JobOpOutput, error) { - if _, err := deps.DB.GetJob(in.ID); err != nil { - return nil, JobOpOutput{}, errors.New("job not found") - } - if err := deps.DB.RetryJob(in.ID); err != nil { - return nil, JobOpOutput{}, err - } - return nil, JobOpOutput{ID: in.ID, Status: "pending"}, nil - }, - ) - }) + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "retry_job", + Title: "Retry Job", + Description: "Reset a terminal job (status=failed) back to pending so the scheduler picks it up on the next tick. attempts is reset to 0.", + Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in JobIDInput) (*mcpsdk.CallToolResult, JobOpOutput, error) { + if _, err := deps.DB.GetJob(in.ID); err != nil { + return nil, JobOpOutput{}, errors.New("job not found") + } + if err := deps.DB.RetryJob(in.ID); err != nil { + return nil, JobOpOutput{}, err + } + return nil, JobOpOutput{ID: in.ID, Status: "pending"}, nil + }, + ) - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "delete_job", - Title: "Delete Job", - Description: "Remove a job row entirely. Pass confirm=true. Use this to clear stuck or duplicate enqueues; for a normal failed job prefer retry_job to keep the audit trail.", - Annotations: &mcpsdk.ToolAnnotations{DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in JobIDInputWithConfirm) (*mcpsdk.CallToolResult, JobOpOutput, error) { - if !in.Confirm { - return nil, JobOpOutput{}, errors.New("delete refused: pass confirm=true") - } - if err := deps.DB.DeleteJob(in.ID); err != nil { - return nil, JobOpOutput{}, err - } - return nil, JobOpOutput{ID: in.ID, Status: "deleted"}, nil - }, - ) - }) + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "delete_job", + Title: "Delete Job", + Description: "Remove a job row entirely. Pass confirm=true. Use this to clear stuck or duplicate enqueues; for a normal failed job prefer retry_job to keep the audit trail.", + Annotations: &mcpsdk.ToolAnnotations{DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in JobIDInputWithConfirm) (*mcpsdk.CallToolResult, JobOpOutput, error) { + if !in.Confirm { + return nil, JobOpOutput{}, errors.New("delete refused: pass confirm=true") + } + if err := deps.DB.DeleteJob(in.ID); err != nil { + return nil, JobOpOutput{}, err + } + return nil, JobOpOutput{ID: in.ID, Status: "deleted"}, nil + }, + ) } diff --git a/backend/internal/mcp/tools_keys.go b/backend/internal/mcp/tools_keys.go index 614d7f2..65e14f8 100644 --- a/backend/internal/mcp/tools_keys.go +++ b/backend/internal/mcp/tools_keys.go @@ -65,106 +65,102 @@ type DeleteAPIKeyInput struct { Confirm bool `json:"confirm"` } -func registerKeyTools(s *mcpsdk.Server, deps Deps, perms permSet) { - gatedAdd(perms, permAdmin, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "list_api_keys", - Title: "List API Keys", - Description: "List all API keys with their metadata (id, prefix, name, permissions, last-used, expiry). Plaintext key values are NEVER returned — they are SHA256-hashed at rest, and the only opportunity to see one is in the response of create_api_key.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, ListAPIKeysOutput, error) { - rows, err := deps.DB.ListAPIKeys() - if err != nil { - return nil, ListAPIKeysOutput{}, err - } - out := ListAPIKeysOutput{Keys: make([]APIKeyView, 0, len(rows))} - for _, k := range rows { - out.Keys = append(out.Keys, toAPIKeyView(k)) - } - return nil, out, nil - }, - ) - }) - - gatedAdd(perms, permAdmin, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "create_api_key", - Title: "Create API Key", - Description: "Mint a new API key. The plaintext value is returned ONLY in this response; the server keeps a SHA256 hash and forgets the plaintext. `permissions` and `expires_in_days` are REQUIRED — least-privilege scope and finite lifetime are the cheapest defenses against a leaked key. Marked destructive because issuing a key with admin permissions is high-blast-radius — confirm with the user what permissions to grant and how long it should live before calling.", - Annotations: &mcpsdk.ToolAnnotations{DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in CreateAPIKeyInput) (*mcpsdk.CallToolResult, CreateAPIKeyOutput, error) { - if strings.TrimSpace(in.Name) == "" { - return nil, CreateAPIKeyOutput{}, errors.New("name is required (human-readable label, e.g. 'ci-deploys')") - } - if len(in.Permissions) == 0 { - return nil, CreateAPIKeyOutput{}, errors.New( - "permissions is required: pick a subset of [invoke, " + - "read, write, admin] matching what the consumer " + - "actually needs. Granting all four was the silent " + - "default and produced over-privileged keys; least-" + - "privilege is the right shape.", - ) - } - if in.ExpiresInDays < 0 { - return nil, CreateAPIKeyOutput{}, errors.New("expires_in_days must be >= 0 (0 = never expires; >0 = days until expiry)") - } - perms := in.Permissions - rawKey := make([]byte, 32) - if _, err := rand.Read(rawKey); err != nil { - return nil, CreateAPIKeyOutput{}, err - } - plaintext := "orva_" + hex.EncodeToString(rawKey) - hash := sha256.Sum256([]byte(plaintext)) - - keyID := ids.New() - prefix := plaintext[:12] - - var expiresAt *time.Time - if in.ExpiresInDays > 0 { - t := time.Now().UTC().Add(time.Duration(in.ExpiresInDays) * 24 * time.Hour) - expiresAt = &t - } - - permsJSON, _ := json.Marshal(perms) - row := &database.APIKey{ - ID: keyID, KeyHash: hex.EncodeToString(hash[:]), - Prefix: prefix, Name: in.Name, - Permissions: string(permsJSON), ExpiresAt: expiresAt, - } - if err := deps.DB.InsertAPIKey(row); err != nil { - return nil, CreateAPIKeyOutput{}, err - } - - return nil, CreateAPIKeyOutput{ - ID: keyID, Key: plaintext, Prefix: prefix, - Name: in.Name, Permissions: perms, - ExpiresAt: expiresAt, CreatedAt: time.Now().UTC(), - }, nil - }, - ) - }) - - gatedAdd(perms, permAdmin, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "delete_api_key", - Title: "Delete API Key", - Description: "Revoke an API key by id. Pass confirm=true. Active sessions using that key fail their next request with 401.", - Annotations: &mcpsdk.ToolAnnotations{DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in DeleteAPIKeyInput) (*mcpsdk.CallToolResult, DeletedOutput, error) { - if !in.Confirm { - return nil, DeletedOutput{}, errors.New("delete refused: pass confirm=true") - } - if err := deps.DB.DeleteAPIKey(in.KeyID); err != nil { - return nil, DeletedOutput{}, err - } - return nil, DeletedOutput{DeletedID: in.KeyID}, nil - }, - ) - }) +func registerKeyTools(rc *regCtx) { + deps := rc.deps + rc.group = "keys" + regAddTool(rc, permAdmin, + &mcpsdk.Tool{ + Name: "list_api_keys", + Title: "List API Keys", + Description: "List all API keys with their metadata (id, prefix, name, permissions, last-used, expiry). Plaintext key values are NEVER returned — they are SHA256-hashed at rest, and the only opportunity to see one is in the response of create_api_key.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, ListAPIKeysOutput, error) { + rows, err := deps.DB.ListAPIKeys() + if err != nil { + return nil, ListAPIKeysOutput{}, err + } + out := ListAPIKeysOutput{Keys: make([]APIKeyView, 0, len(rows))} + for _, k := range rows { + out.Keys = append(out.Keys, toAPIKeyView(k)) + } + return nil, out, nil + }, + ) + + regAddTool(rc, permAdmin, + &mcpsdk.Tool{ + Name: "create_api_key", + Title: "Create API Key", + Description: "Mint a new API key. The plaintext value is returned ONLY in this response; the server keeps a SHA256 hash and forgets the plaintext. `permissions` and `expires_in_days` are REQUIRED — least-privilege scope and finite lifetime are the cheapest defenses against a leaked key. Marked destructive because issuing a key with admin permissions is high-blast-radius — confirm with the user what permissions to grant and how long it should live before calling.", + Annotations: &mcpsdk.ToolAnnotations{DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in CreateAPIKeyInput) (*mcpsdk.CallToolResult, CreateAPIKeyOutput, error) { + if strings.TrimSpace(in.Name) == "" { + return nil, CreateAPIKeyOutput{}, errors.New("name is required (human-readable label, e.g. 'ci-deploys')") + } + if len(in.Permissions) == 0 { + return nil, CreateAPIKeyOutput{}, errors.New( + "permissions is required: pick a subset of [invoke, " + + "read, write, admin] matching what the consumer " + + "actually needs. Granting all four was the silent " + + "default and produced over-privileged keys; least-" + + "privilege is the right shape.", + ) + } + if in.ExpiresInDays < 0 { + return nil, CreateAPIKeyOutput{}, errors.New("expires_in_days must be >= 0 (0 = never expires; >0 = days until expiry)") + } + perms := in.Permissions + rawKey := make([]byte, 32) + if _, err := rand.Read(rawKey); err != nil { + return nil, CreateAPIKeyOutput{}, err + } + plaintext := "orva_" + hex.EncodeToString(rawKey) + hash := sha256.Sum256([]byte(plaintext)) + + keyID := ids.New() + prefix := plaintext[:12] + + var expiresAt *time.Time + if in.ExpiresInDays > 0 { + t := time.Now().UTC().Add(time.Duration(in.ExpiresInDays) * 24 * time.Hour) + expiresAt = &t + } + + permsJSON, _ := json.Marshal(perms) + row := &database.APIKey{ + ID: keyID, KeyHash: hex.EncodeToString(hash[:]), + Prefix: prefix, Name: in.Name, + Permissions: string(permsJSON), ExpiresAt: expiresAt, + } + if err := deps.DB.InsertAPIKey(row); err != nil { + return nil, CreateAPIKeyOutput{}, err + } + + return nil, CreateAPIKeyOutput{ + ID: keyID, Key: plaintext, Prefix: prefix, + Name: in.Name, Permissions: perms, + ExpiresAt: expiresAt, CreatedAt: time.Now().UTC(), + }, nil + }, + ) + + regAddTool(rc, permAdmin, + &mcpsdk.Tool{ + Name: "delete_api_key", + Title: "Delete API Key", + Description: "Revoke an API key by id. Pass confirm=true. Active sessions using that key fail their next request with 401.", + Annotations: &mcpsdk.ToolAnnotations{DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in DeleteAPIKeyInput) (*mcpsdk.CallToolResult, DeletedOutput, error) { + if !in.Confirm { + return nil, DeletedOutput{}, errors.New("delete refused: pass confirm=true") + } + if err := deps.DB.DeleteAPIKey(in.KeyID); err != nil { + return nil, DeletedOutput{}, err + } + return nil, DeletedOutput{DeletedID: in.KeyID}, nil + }, + ) } diff --git a/backend/internal/mcp/tools_kv.go b/backend/internal/mcp/tools_kv.go index f7440cc..0d4b702 100644 --- a/backend/internal/mcp/tools_kv.go +++ b/backend/internal/mcp/tools_kv.go @@ -171,188 +171,179 @@ type KVCASInput struct { TTLSeconds int `json:"ttl_seconds,omitempty"` } type KVCASOutput struct { - OK bool `json:"ok"` - Current *KVValue `json:"current,omitempty" jsonschema:"on !ok, the value currently stored so callers can retry with a fresh expectation"` + OK bool `json:"ok"` + Current *KVValue `json:"current,omitempty" jsonschema:"on !ok, the value currently stored so callers can retry with a fresh expectation"` } -func registerKVTools(s *mcpsdk.Server, deps Deps, perms permSet) { - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "kv_get", - Title: "KV Get", - Description: "Read a value from a function's per-namespace KV store. Returns found=false if the key is missing or has expired (TTL elapsed).", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in KVGetInput) (*mcpsdk.CallToolResult, KVGetOutput, error) { - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, KVGetOutput{}, err - } - key := strings.TrimSpace(in.Key) - if key == "" { - return nil, KVGetOutput{}, errors.New("key is required") - } - entry, err := deps.DB.KVGet(fn.ID, key) - if errors.Is(err, database.ErrKVNotFound) { - return nil, KVGetOutput{Found: false}, nil - } - if err != nil { - return nil, KVGetOutput{}, err - } - view := toKVView(entry) - return nil, KVGetOutput{Found: true, Entry: &view}, nil - }, - ) - }) +func registerKVTools(rc *regCtx) { + deps := rc.deps + rc.group = "kv" - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "kv_put", - Title: "KV Put", - Description: "Write a value to a function's KV store. value can be any JSON-serializable type; it's stored as JSON and returned by kv_get with the same shape. Optional ttl_seconds expires the key automatically.", - Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in KVPutInput) (*mcpsdk.CallToolResult, KVPutOutput, error) { - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, KVPutOutput{}, err - } - key := strings.TrimSpace(in.Key) - if key == "" { - return nil, KVPutOutput{}, errors.New("key is required") - } - body, err := encodeKVValue(in.Value) - if err != nil { - return nil, KVPutOutput{}, err - } - if err := deps.DB.KVPut(fn.ID, key, body, in.TTLSeconds); err != nil { - return nil, KVPutOutput{}, err - } - return nil, KVPutOutput{Key: key, TTLSeconds: in.TTLSeconds}, nil - }, - ) - }) + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "kv_get", + Title: "KV Get", + Description: "Read a value from a function's per-namespace KV store. Returns found=false if the key is missing or has expired (TTL elapsed).", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in KVGetInput) (*mcpsdk.CallToolResult, KVGetOutput, error) { + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, KVGetOutput{}, err + } + key := strings.TrimSpace(in.Key) + if key == "" { + return nil, KVGetOutput{}, errors.New("key is required") + } + entry, err := deps.DB.KVGet(fn.ID, key) + if errors.Is(err, database.ErrKVNotFound) { + return nil, KVGetOutput{Found: false}, nil + } + if err != nil { + return nil, KVGetOutput{}, err + } + view := toKVView(entry) + return nil, KVGetOutput{Found: true, Entry: &view}, nil + }, + ) - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "kv_delete", - Title: "KV Delete", - Description: "Remove a single key from a function's KV store. Idempotent — returns ok even if the key never existed.", - Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in KVDeleteInput) (*mcpsdk.CallToolResult, KVDeleteOutput, error) { - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, KVDeleteOutput{}, err - } - if err := deps.DB.KVDelete(fn.ID, in.Key); err != nil { - return nil, KVDeleteOutput{}, err - } - return nil, KVDeleteOutput{Status: "deleted", Key: in.Key}, nil - }, - ) - }) + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "kv_put", + Title: "KV Put", + Description: "Write a value to a function's KV store. value can be any JSON-serializable type; it's stored as JSON and returned by kv_get with the same shape. Optional ttl_seconds expires the key automatically.", + Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in KVPutInput) (*mcpsdk.CallToolResult, KVPutOutput, error) { + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, KVPutOutput{}, err + } + key := strings.TrimSpace(in.Key) + if key == "" { + return nil, KVPutOutput{}, errors.New("key is required") + } + body, err := encodeKVValue(in.Value) + if err != nil { + return nil, KVPutOutput{}, err + } + if err := deps.DB.KVPut(fn.ID, key, body, in.TTLSeconds); err != nil { + return nil, KVPutOutput{}, err + } + return nil, KVPutOutput{Key: key, TTLSeconds: in.TTLSeconds}, nil + }, + ) - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "kv_list", - Title: "KV List", - Description: "List a function's KV keys, optionally filtered by prefix. Useful for inspecting what state a function has accumulated. Expired keys are excluded.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in KVListInput) (*mcpsdk.CallToolResult, KVListOutput, error) { - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, KVListOutput{}, err - } - page, err := deps.DB.KVListWithCursor(fn.ID, in.Prefix, in.Cursor, in.Limit) - if err != nil { - return nil, KVListOutput{}, err - } - out := KVListOutput{ - Entries: make([]KVView, 0, len(page.Entries)), - NextCursor: page.NextCursor, - } - for _, e := range page.Entries { - out.Entries = append(out.Entries, toKVView(e)) - } - return nil, out, nil - }, - ) - }) + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "kv_delete", + Title: "KV Delete", + Description: "Remove a single key from a function's KV store. Idempotent — returns ok even if the key never existed.", + Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in KVDeleteInput) (*mcpsdk.CallToolResult, KVDeleteOutput, error) { + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, KVDeleteOutput{}, err + } + if err := deps.DB.KVDelete(fn.ID, in.Key); err != nil { + return nil, KVDeleteOutput{}, err + } + return nil, KVDeleteOutput{Status: "deleted", Key: in.Key}, nil + }, + ) - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "kv_incr", - Title: "KV Increment", - Description: "Atomically increment an integer counter. Missing keys are treated as 0; pass a negative delta to decrement. Use this instead of read+put when multiple writers can update the same counter concurrently.", - Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: false, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in KVIncrInput) (*mcpsdk.CallToolResult, KVIncrOutput, error) { - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, KVIncrOutput{}, err - } - key := strings.TrimSpace(in.Key) - if key == "" { - return nil, KVIncrOutput{}, errors.New("key is required") - } - delta := in.Delta - if delta == 0 { - delta = 1 - } - next, err := deps.DB.KVIncr(fn.ID, key, delta, in.TTLSeconds) - if err != nil { - return nil, KVIncrOutput{}, err - } - return nil, KVIncrOutput{Value: next}, nil - }, - ) - }) + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "kv_list", + Title: "KV List", + Description: "List a function's KV keys, optionally filtered by prefix. Useful for inspecting what state a function has accumulated. Expired keys are excluded.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in KVListInput) (*mcpsdk.CallToolResult, KVListOutput, error) { + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, KVListOutput{}, err + } + page, err := deps.DB.KVListWithCursor(fn.ID, in.Prefix, in.Cursor, in.Limit) + if err != nil { + return nil, KVListOutput{}, err + } + out := KVListOutput{ + Entries: make([]KVView, 0, len(page.Entries)), + NextCursor: page.NextCursor, + } + for _, e := range page.Entries { + out.Entries = append(out.Entries, toKVView(e)) + } + return nil, out, nil + }, + ) - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "kv_cas", - Title: "KV Compare-and-swap", - Description: "Atomically swap a key's value only if the current value matches Expected. Useful for safe read-modify-write loops where multiple writers could otherwise overwrite each other. Returns ok=false plus the current value when the precondition fails.", - Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: false, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in KVCASInput) (*mcpsdk.CallToolResult, KVCASOutput, error) { - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, KVCASOutput{}, err - } - key := strings.TrimSpace(in.Key) - if key == "" { - return nil, KVCASOutput{}, errors.New("key is required") - } - expected, err := encodeKVValue(in.Expected) - if err != nil { - return nil, KVCASOutput{}, err - } - next, err := encodeKVValue(in.New) - if err != nil { - return nil, KVCASOutput{}, err - } - ok, current, err := deps.DB.KVCAS(fn.ID, key, expected, next, in.TTLSeconds) - if err != nil { - return nil, KVCASOutput{}, err - } - out := KVCASOutput{OK: ok} - if !ok && current != nil { - // Surface the raw bytes back through KVValue so the - // schema stays self-describing for MCP clients. - decoded := decodeKVValue(current) - out.Current = &decoded - } - return nil, out, nil - }, - ) - }) + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "kv_incr", + Title: "KV Increment", + Description: "Atomically increment an integer counter. Missing keys are treated as 0; pass a negative delta to decrement. Use this instead of read+put when multiple writers can update the same counter concurrently.", + Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: false, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in KVIncrInput) (*mcpsdk.CallToolResult, KVIncrOutput, error) { + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, KVIncrOutput{}, err + } + key := strings.TrimSpace(in.Key) + if key == "" { + return nil, KVIncrOutput{}, errors.New("key is required") + } + delta := in.Delta + if delta == 0 { + delta = 1 + } + next, err := deps.DB.KVIncr(fn.ID, key, delta, in.TTLSeconds) + if err != nil { + return nil, KVIncrOutput{}, err + } + return nil, KVIncrOutput{Value: next}, nil + }, + ) + + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "kv_cas", + Title: "KV Compare-and-swap", + Description: "Atomically swap a key's value only if the current value matches Expected. Useful for safe read-modify-write loops where multiple writers could otherwise overwrite each other. Returns ok=false plus the current value when the precondition fails.", + Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: false, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in KVCASInput) (*mcpsdk.CallToolResult, KVCASOutput, error) { + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, KVCASOutput{}, err + } + key := strings.TrimSpace(in.Key) + if key == "" { + return nil, KVCASOutput{}, errors.New("key is required") + } + expected, err := encodeKVValue(in.Expected) + if err != nil { + return nil, KVCASOutput{}, err + } + next, err := encodeKVValue(in.New) + if err != nil { + return nil, KVCASOutput{}, err + } + ok, current, err := deps.DB.KVCAS(fn.ID, key, expected, next, in.TTLSeconds) + if err != nil { + return nil, KVCASOutput{}, err + } + out := KVCASOutput{OK: ok} + if !ok && current != nil { + // Surface the raw bytes back through KVValue so the + // schema stays self-describing for MCP clients. + decoded := decodeKVValue(current) + out.Current = &decoded + } + return nil, out, nil + }, + ) } diff --git a/backend/internal/mcp/tools_pool.go b/backend/internal/mcp/tools_pool.go index 968a7c3..8635125 100644 --- a/backend/internal/mcp/tools_pool.go +++ b/backend/internal/mcp/tools_pool.go @@ -37,76 +37,74 @@ func toPoolConfigView(c *database.PoolConfig) PoolConfigView { } } -func registerPoolTools(s *mcpsdk.Server, deps Deps, perms permSet) { - gatedAdd(perms, permAdmin, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "get_pool_config", - Title: "Get Pool Config", - Description: "Get the autoscaler pool config for a function (min_warm, max_warm, idle_ttl, target_concurrency, scale_to_zero). Returns nulls/defaults if no override is configured.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in GetPoolConfigInput) (*mcpsdk.CallToolResult, PoolConfigView, error) { - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, PoolConfigView{}, err - } - cfg, err := deps.DB.GetPoolConfig(fn.ID) - if err != nil { - // no row = use defaults - return nil, PoolConfigView{ - FunctionID: fn.ID, MinWarm: 1, MaxWarm: 50, - IdleTTLSeconds: 600, TargetConcurrency: 10, - }, nil - } - return nil, toPoolConfigView(cfg), nil - }, - ) - }) +func registerPoolTools(rc *regCtx) { + deps := rc.deps + rc.group = "pool" + regAddTool(rc, permAdmin, + &mcpsdk.Tool{ + Name: "get_pool_config", + Title: "Get Pool Config", + Description: "Get the autoscaler pool config for a function (min_warm, max_warm, idle_ttl, target_concurrency, scale_to_zero). Returns nulls/defaults if no override is configured.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in GetPoolConfigInput) (*mcpsdk.CallToolResult, PoolConfigView, error) { + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, PoolConfigView{}, err + } + cfg, err := deps.DB.GetPoolConfig(fn.ID) + if err != nil { + // no row = use defaults + return nil, PoolConfigView{ + FunctionID: fn.ID, MinWarm: 1, MaxWarm: 50, + IdleTTLSeconds: 600, TargetConcurrency: 10, + }, nil + } + return nil, toPoolConfigView(cfg), nil + }, + ) - gatedAdd(perms, permAdmin, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "set_pool_config", - Title: "Set Pool Config", - Description: "Tune the autoscaler for a function. Any field omitted retains its current value. Changes apply to new sandbox spawns; existing warm workers keep their behavior until recycled.", - Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in SetPoolConfigInput) (*mcpsdk.CallToolResult, PoolConfigView, error) { - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, PoolConfigView{}, err - } - cfg, err := deps.DB.GetPoolConfig(fn.ID) - if err != nil { - cfg = &database.PoolConfig{ - FunctionID: fn.ID, MinWarm: 1, MaxWarm: 50, - IdleTTLS: 600, TargetConcurrency: 10, - } - } - if in.MinWarm != nil { - cfg.MinWarm = *in.MinWarm - } - if in.MaxWarm != nil { - cfg.MaxWarm = *in.MaxWarm - } - if in.IdleTTLSeconds != nil { - cfg.IdleTTLS = *in.IdleTTLSeconds - } - if in.TargetConcurrency != nil { - cfg.TargetConcurrency = *in.TargetConcurrency - } - if in.ScaleToZero != nil { - cfg.ScaleToZero = *in.ScaleToZero - } - if err := deps.DB.UpsertPoolConfig(cfg); err != nil { - return nil, PoolConfigView{}, err - } - if deps.PoolMgr != nil { - deps.PoolMgr.RefreshForDeploy(fn.ID) + regAddTool(rc, permAdmin, + &mcpsdk.Tool{ + Name: "set_pool_config", + Title: "Set Pool Config", + Description: "Tune the autoscaler for a function. Any field omitted retains its current value. Changes apply to new sandbox spawns; existing warm workers keep their behavior until recycled.", + Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in SetPoolConfigInput) (*mcpsdk.CallToolResult, PoolConfigView, error) { + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, PoolConfigView{}, err + } + cfg, err := deps.DB.GetPoolConfig(fn.ID) + if err != nil { + cfg = &database.PoolConfig{ + FunctionID: fn.ID, MinWarm: 1, MaxWarm: 50, + IdleTTLS: 600, TargetConcurrency: 10, } - return nil, toPoolConfigView(cfg), nil - }, - ) - }) + } + if in.MinWarm != nil { + cfg.MinWarm = *in.MinWarm + } + if in.MaxWarm != nil { + cfg.MaxWarm = *in.MaxWarm + } + if in.IdleTTLSeconds != nil { + cfg.IdleTTLS = *in.IdleTTLSeconds + } + if in.TargetConcurrency != nil { + cfg.TargetConcurrency = *in.TargetConcurrency + } + if in.ScaleToZero != nil { + cfg.ScaleToZero = *in.ScaleToZero + } + if err := deps.DB.UpsertPoolConfig(cfg); err != nil { + return nil, PoolConfigView{}, err + } + if deps.PoolMgr != nil { + deps.PoolMgr.RefreshForDeploy(fn.ID) + } + return nil, toPoolConfigView(cfg), nil + }, + ) } diff --git a/backend/internal/mcp/tools_routes.go b/backend/internal/mcp/tools_routes.go index dbbaf96..5e01616 100644 --- a/backend/internal/mcp/tools_routes.go +++ b/backend/internal/mcp/tools_routes.go @@ -38,85 +38,82 @@ type DeleteRouteInput struct { var reservedPrefixes = []string{"/api/", "/auth/", "/web/", "/_orva/"} -func registerRouteTools(s *mcpsdk.Server, deps Deps, perms permSet) { - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "list_routes", - Title: "List Routes", - Description: "List all custom routes (operator-defined URL → function mappings). Each entry has the path, target function_id, allowed methods, and creation time.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, ListRoutesOutput, error) { - rows, err := deps.DB.ListRoutes() - if err != nil { - return nil, ListRoutesOutput{}, err - } - out := ListRoutesOutput{Routes: make([]RouteView, 0, len(rows))} - for _, r := range rows { - out.Routes = append(out.Routes, RouteView{ - Path: r.Path, FunctionID: r.FunctionID, Methods: r.Methods, CreatedAt: r.CreatedAt, - }) - } - return nil, out, nil - }, - ) - }) +func registerRouteTools(rc *regCtx) { + deps := rc.deps + rc.group = "routes" - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "set_route", - Title: "Set Route", - Description: "Create or update a custom route. Use exact paths (/webhooks/stripe) for fixed URLs, or prefix paths (/shortener/*) when the function should see sub-paths. Reserved prefixes (/api/, /auth/, /web/, /_orva/) are rejected.", - Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in SetRouteInput) (*mcpsdk.CallToolResult, RouteOpOutput, error) { - path := strings.TrimSpace(in.Path) - if !strings.HasPrefix(path, "/") { - return nil, RouteOpOutput{}, errors.New("path must start with /") - } - for _, p := range reservedPrefixes { - if strings.HasPrefix(path, p) { - return nil, RouteOpOutput{}, errors.New("path conflicts with reserved prefix " + p) - } - } - if strings.Contains(path, "*") && !strings.HasSuffix(path, "/*") { - return nil, RouteOpOutput{}, errors.New("wildcard must be the final segment ('/prefix/*')") - } - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, RouteOpOutput{}, err - } - methods := in.Methods - if methods == "" { - methods = "*" - } - if err := deps.DB.UpsertRoute(path, fn.ID, methods); err != nil { - return nil, RouteOpOutput{}, err - } - return nil, RouteOpOutput{Path: path, FunctionID: fn.ID}, nil - }, - ) - }) + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "list_routes", + Title: "List Routes", + Description: "List all custom routes (operator-defined URL → function mappings). Each entry has the path, target function_id, allowed methods, and creation time.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, ListRoutesOutput, error) { + rows, err := deps.DB.ListRoutes() + if err != nil { + return nil, ListRoutesOutput{}, err + } + out := ListRoutesOutput{Routes: make([]RouteView, 0, len(rows))} + for _, r := range rows { + out.Routes = append(out.Routes, RouteView{ + Path: r.Path, FunctionID: r.FunctionID, Methods: r.Methods, CreatedAt: r.CreatedAt, + }) + } + return nil, out, nil + }, + ) - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "delete_route", - Title: "Delete Route", - Description: "Remove a custom route by path. Pass confirm=true. Does not affect the function the route pointed at.", - Annotations: &mcpsdk.ToolAnnotations{DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in DeleteRouteInput) (*mcpsdk.CallToolResult, RouteOpOutput, error) { - if !in.Confirm { - return nil, RouteOpOutput{}, errors.New("delete refused: pass confirm=true") + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "set_route", + Title: "Set Route", + Description: "Create or update a custom route. Use exact paths (/webhooks/stripe) for fixed URLs, or prefix paths (/shortener/*) when the function should see sub-paths. Reserved prefixes (/api/, /auth/, /web/, /_orva/) are rejected.", + Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in SetRouteInput) (*mcpsdk.CallToolResult, RouteOpOutput, error) { + path := strings.TrimSpace(in.Path) + if !strings.HasPrefix(path, "/") { + return nil, RouteOpOutput{}, errors.New("path must start with /") + } + for _, p := range reservedPrefixes { + if strings.HasPrefix(path, p) { + return nil, RouteOpOutput{}, errors.New("path conflicts with reserved prefix " + p) } - if err := deps.DB.DeleteRoute(in.Path); err != nil { - return nil, RouteOpOutput{}, err - } - return nil, RouteOpOutput{Path: in.Path}, nil - }, - ) - }) + } + if strings.Contains(path, "*") && !strings.HasSuffix(path, "/*") { + return nil, RouteOpOutput{}, errors.New("wildcard must be the final segment ('/prefix/*')") + } + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, RouteOpOutput{}, err + } + methods := in.Methods + if methods == "" { + methods = "*" + } + if err := deps.DB.UpsertRoute(path, fn.ID, methods); err != nil { + return nil, RouteOpOutput{}, err + } + return nil, RouteOpOutput{Path: path, FunctionID: fn.ID}, nil + }, + ) + + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "delete_route", + Title: "Delete Route", + Description: "Remove a custom route by path. Pass confirm=true. Does not affect the function the route pointed at.", + Annotations: &mcpsdk.ToolAnnotations{DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in DeleteRouteInput) (*mcpsdk.CallToolResult, RouteOpOutput, error) { + if !in.Confirm { + return nil, RouteOpOutput{}, errors.New("delete refused: pass confirm=true") + } + if err := deps.DB.DeleteRoute(in.Path); err != nil { + return nil, RouteOpOutput{}, err + } + return nil, RouteOpOutput{Path: in.Path}, nil + }, + ) } diff --git a/backend/internal/mcp/tools_secrets.go b/backend/internal/mcp/tools_secrets.go index e43fa04..7923faf 100644 --- a/backend/internal/mcp/tools_secrets.go +++ b/backend/internal/mcp/tools_secrets.go @@ -44,84 +44,81 @@ type DeleteSecretInput struct { Confirm bool `json:"confirm"` } -func registerSecretTools(s *mcpsdk.Server, deps Deps, perms permSet) { - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "list_secrets", - Title: "List Secrets", - Description: "List the NAMES of secrets configured for a function. Values are write-only — they are encrypted at rest and decrypted only into the sandbox process at invocation time. There is no API path, MCP tool, or UI screen that can read a stored secret value.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in ListSecretsInput) (*mcpsdk.CallToolResult, ListSecretsOutput, error) { - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, ListSecretsOutput{}, err - } - names, err := deps.Secrets.List(fn.ID) - if err != nil { - return nil, ListSecretsOutput{}, err - } - if names == nil { - names = []string{} - } - return nil, ListSecretsOutput{FunctionID: fn.ID, Names: names}, nil - }, - ) - }) +func registerSecretTools(rc *regCtx) { + deps := rc.deps + rc.group = "secrets" - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "set_secret", - Title: "Set Secret", - Description: "Store or update a secret for a function. Value is encrypted at rest (AES-256-GCM). Idempotent — re-setting the same key overwrites the prior value. After the call returns the value is unreadable through any API. The function's warm pool is drained so the next invoke spawns with the new value.", - Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in SetSecretInput) (*mcpsdk.CallToolResult, SecretOpOutput, error) { - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, SecretOpOutput{}, err - } - key := strings.TrimSpace(in.Key) - if key == "" { - return nil, SecretOpOutput{}, errors.New("key is required") - } - if err := deps.Secrets.Upsert(fn.ID, key, in.Value); err != nil { - return nil, SecretOpOutput{}, err - } - if deps.PoolMgr != nil { - deps.PoolMgr.RefreshForDeploy(fn.ID) - } - return nil, SecretOpOutput{Key: key}, nil - }, - ) - }) + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "list_secrets", + Title: "List Secrets", + Description: "List the NAMES of secrets configured for a function. Values are write-only — they are encrypted at rest and decrypted only into the sandbox process at invocation time. There is no API path, MCP tool, or UI screen that can read a stored secret value.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in ListSecretsInput) (*mcpsdk.CallToolResult, ListSecretsOutput, error) { + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, ListSecretsOutput{}, err + } + names, err := deps.Secrets.List(fn.ID) + if err != nil { + return nil, ListSecretsOutput{}, err + } + if names == nil { + names = []string{} + } + return nil, ListSecretsOutput{FunctionID: fn.ID, Names: names}, nil + }, + ) - gatedAdd(perms, permWrite, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "delete_secret", - Title: "Delete Secret", - Description: "Delete a secret from a function by name. Pass confirm=true. The function's warm pool is drained so the next invoke loses access immediately.", - Annotations: &mcpsdk.ToolAnnotations{DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in DeleteSecretInput) (*mcpsdk.CallToolResult, SecretOpOutput, error) { - if !in.Confirm { - return nil, SecretOpOutput{}, errors.New("delete refused: pass confirm=true") - } - fn, err := resolveFunction(deps, in.FunctionID) - if err != nil { - return nil, SecretOpOutput{}, err - } - if err := deps.Secrets.Delete(fn.ID, in.Key); err != nil { - return nil, SecretOpOutput{}, err - } - if deps.PoolMgr != nil { - deps.PoolMgr.RefreshForDeploy(fn.ID) - } - return nil, SecretOpOutput{Key: in.Key}, nil - }, - ) - }) + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "set_secret", + Title: "Set Secret", + Description: "Store or update a secret for a function. Value is encrypted at rest (AES-256-GCM). Idempotent — re-setting the same key overwrites the prior value. After the call returns the value is unreadable through any API. The function's warm pool is drained so the next invoke spawns with the new value.", + Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in SetSecretInput) (*mcpsdk.CallToolResult, SecretOpOutput, error) { + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, SecretOpOutput{}, err + } + key := strings.TrimSpace(in.Key) + if key == "" { + return nil, SecretOpOutput{}, errors.New("key is required") + } + if err := deps.Secrets.Upsert(fn.ID, key, in.Value); err != nil { + return nil, SecretOpOutput{}, err + } + if deps.PoolMgr != nil { + deps.PoolMgr.RefreshForDeploy(fn.ID) + } + return nil, SecretOpOutput{Key: key}, nil + }, + ) + + regAddTool(rc, permWrite, + &mcpsdk.Tool{ + Name: "delete_secret", + Title: "Delete Secret", + Description: "Delete a secret from a function by name. Pass confirm=true. The function's warm pool is drained so the next invoke loses access immediately.", + Annotations: &mcpsdk.ToolAnnotations{DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in DeleteSecretInput) (*mcpsdk.CallToolResult, SecretOpOutput, error) { + if !in.Confirm { + return nil, SecretOpOutput{}, errors.New("delete refused: pass confirm=true") + } + fn, err := resolveFunction(deps, in.FunctionID) + if err != nil { + return nil, SecretOpOutput{}, err + } + if err := deps.Secrets.Delete(fn.ID, in.Key); err != nil { + return nil, SecretOpOutput{}, err + } + if deps.PoolMgr != nil { + deps.PoolMgr.RefreshForDeploy(fn.ID) + } + return nil, SecretOpOutput{Key: in.Key}, nil + }, + ) } diff --git a/backend/internal/mcp/tools_system.go b/backend/internal/mcp/tools_system.go index a8881ca..5aad14d 100644 --- a/backend/internal/mcp/tools_system.go +++ b/backend/internal/mcp/tools_system.go @@ -37,112 +37,104 @@ type SystemHealthOutput struct { // to keep the wire-up surface small. var startTime = time.Now() -func registerSystemTools(s *mcpsdk.Server, deps Deps, perms permSet) { - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "system_health", - Title: "System Health", - Description: "Health check for the Orva instance. Returns version, uptime, sandbox counters, and host resources. Use this to confirm an Orva instance is reachable before doing anything else.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, SystemHealthOutput, error) { - var active, total int64 - if deps.Proxy != nil && deps.Proxy.Sandbox != nil { - active, total = deps.Proxy.Sandbox.Stats() - } - return nil, SystemHealthOutput{ - Status: "healthy", - Version: orDefault(deps.Version, "0.1.0"), - UptimeSeconds: int64(time.Since(startTime).Seconds()), - SandboxActive: active, - SandboxLifetime: total, - HostNumCPU: runtime.NumCPU(), - HostNumGoroutines: runtime.NumGoroutine(), - }, nil - }, - ) - }) +func registerSystemTools(rc *regCtx) { + deps := rc.deps + rc.group = "system" + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "system_health", + Title: "System Health", + Description: "Health check for the Orva instance. Returns version, uptime, sandbox counters, and host resources. Use this to confirm an Orva instance is reachable before doing anything else.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, SystemHealthOutput, error) { + var active, total int64 + if deps.Proxy != nil && deps.Proxy.Sandbox != nil { + active, total = deps.Proxy.Sandbox.Stats() + } + return nil, SystemHealthOutput{ + Status: "healthy", + Version: orDefault(deps.Version, "0.1.0"), + UptimeSeconds: int64(time.Since(startTime).Seconds()), + SandboxActive: active, + SandboxLifetime: total, + HostNumCPU: runtime.NumCPU(), + HostNumGoroutines: runtime.NumGoroutine(), + }, nil + }, + ) // ─── system_metrics ────────────────────────────────────────────── - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "system_metrics", - Title: "System Metrics", - Description: "Return a snapshot of Orva's invocation/build/latency counters and per-function pool stats. Useful for an agent that wants to see how loaded the platform is or which functions are hot.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, SystemMetricsOutput, error) { - return nil, buildSystemMetrics(deps), nil - }, - ) - }) + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "system_metrics", + Title: "System Metrics", + Description: "Return a snapshot of Orva's invocation/build/latency counters and per-function pool stats. Useful for an agent that wants to see how loaded the platform is or which functions are hot.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, SystemMetricsOutput, error) { + return nil, buildSystemMetrics(deps), nil + }, + ) // ─── system_storage ────────────────────────────────────────────── - gatedAdd(perms, permAdmin, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "system_storage", - Title: "System Storage", - Description: "Return on-disk sizes for orva.db, the WAL sidecar, and the functions/ tree. Useful before deciding to run system_vacuum — db_free_pages × db_page_size is the upper bound on reclaimable bytes.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, SystemStorageOutput, error) { - return nil, buildSystemStorage(deps), nil - }, - ) - }) + regAddTool(rc, permAdmin, + &mcpsdk.Tool{ + Name: "system_storage", + Title: "System Storage", + Description: "Return on-disk sizes for orva.db, the WAL sidecar, and the functions/ tree. Useful before deciding to run system_vacuum — db_free_pages × db_page_size is the upper bound on reclaimable bytes.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, SystemStorageOutput, error) { + return nil, buildSystemStorage(deps), nil + }, + ) // ─── system_vacuum ────────────────────────────────────────────── - gatedAdd(perms, permAdmin, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "system_vacuum", - Title: "System Vacuum", - Description: "Run PRAGMA wal_checkpoint(TRUNCATE) followed by VACUUM on orva.db. DESTRUCTIVE: holds an exclusive lock and rewrites the database; every other writer blocks until it returns. Pass confirm=true to actually run; without confirm the tool returns the would-be reclaimable bytes without touching the DB.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: false, DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in SystemVacuumInput) (*mcpsdk.CallToolResult, SystemVacuumOutput, error) { - if !in.Confirm { - info := buildSystemStorage(deps) - return nil, SystemVacuumOutput{ - DryRun: true, - BeforeBytes: info.DBBytes, - ReclaimableBytes: info.DBFreePages * info.DBPageSize, - }, nil - } - return runMCPVacuum(deps) - }, - ) - }) + regAddTool(rc, permAdmin, + &mcpsdk.Tool{ + Name: "system_vacuum", + Title: "System Vacuum", + Description: "Run PRAGMA wal_checkpoint(TRUNCATE) followed by VACUUM on orva.db. DESTRUCTIVE: holds an exclusive lock and rewrites the database; every other writer blocks until it returns. Pass confirm=true to actually run; without confirm the tool returns the would-be reclaimable bytes without touching the DB.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: false, DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in SystemVacuumInput) (*mcpsdk.CallToolResult, SystemVacuumOutput, error) { + if !in.Confirm { + info := buildSystemStorage(deps) + return nil, SystemVacuumOutput{ + DryRun: true, + BeforeBytes: info.DBBytes, + ReclaimableBytes: info.DBFreePages * info.DBPageSize, + }, nil + } + return runMCPVacuum(deps) + }, + ) // ─── list_runtimes ────────────────────────────────────────────── - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "list_runtimes", - Title: "List Runtimes", - Description: "List the language runtimes Orva supports (Node.js, Python — specific minor versions). Each entry includes its id (use as the `runtime` field on create_function), display name, default entrypoint filename, and accepted file extensions.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, ListRuntimesOutput, error) { - return nil, ListRuntimesOutput{Runtimes: supportedRuntimes()}, nil - }, - ) - }) + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "list_runtimes", + Title: "List Runtimes", + Description: "List the language runtimes Orva supports (Node.js, Python — specific minor versions). Each entry includes its id (use as the `runtime` field on create_function), display name, default entrypoint filename, and accepted file extensions.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, ListRuntimesOutput, error) { + return nil, ListRuntimesOutput{Runtimes: supportedRuntimes()}, nil + }, + ) } // ─── shared types ────────────────────────────────────────────────── type SystemMetricsOutput struct { - UptimeSeconds int64 `json:"uptime_seconds"` - Totals MetricsTotals `json:"totals"` - LatencyMS MetricsLatency `json:"latency_ms"` - ActiveRequests int64 `json:"active_requests"` - SandboxActive int64 `json:"sandbox_active"` - BuildQueue MetricsBuildQ `json:"build_queue"` - Pools []MetricsPool `json:"pools"` + UptimeSeconds int64 `json:"uptime_seconds"` + Totals MetricsTotals `json:"totals"` + LatencyMS MetricsLatency `json:"latency_ms"` + ActiveRequests int64 `json:"active_requests"` + SandboxActive int64 `json:"sandbox_active"` + BuildQueue MetricsBuildQ `json:"build_queue"` + Pools []MetricsPool `json:"pools"` } type MetricsTotals struct { diff --git a/backend/internal/mcp/tools_traces.go b/backend/internal/mcp/tools_traces.go index 568e5ae..a86ee39 100644 --- a/backend/internal/mcp/tools_traces.go +++ b/backend/internal/mcp/tools_traces.go @@ -65,18 +65,18 @@ type LogEntryRow struct { } type GetTraceOutput struct { - TraceID string `json:"trace_id"` - RootSpanID string `json:"root_span_id,omitempty"` - RootFunctionID string `json:"root_function_id,omitempty"` - Trigger string `json:"trigger,omitempty"` - StartedAt string `json:"started_at"` - TotalDurationMS int64 `json:"total_duration_ms"` - Status string `json:"status"` - HasOutlier bool `json:"has_outlier"` - SpanCount int `json:"span_count"` - Spans []SpanRow `json:"spans"` - UserSpans []UserSpanRow `json:"user_spans"` - LogEntries []LogEntryRow `json:"log_entries"` + TraceID string `json:"trace_id"` + RootSpanID string `json:"root_span_id,omitempty"` + RootFunctionID string `json:"root_function_id,omitempty"` + Trigger string `json:"trigger,omitempty"` + StartedAt string `json:"started_at"` + TotalDurationMS int64 `json:"total_duration_ms"` + Status string `json:"status"` + HasOutlier bool `json:"has_outlier"` + SpanCount int `json:"span_count"` + Spans []SpanRow `json:"spans"` + UserSpans []UserSpanRow `json:"user_spans"` + LogEntries []LogEntryRow `json:"log_entries"` } type ListTracesInput struct { @@ -110,176 +110,173 @@ type GetFunctionBaselineInput struct { FunctionID string `json:"function_id" jsonschema:"the function id (UUID) or friendly name"` } -func registerTraceTools(s *mcpsdk.Server, deps Deps, perms permSet) { - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "get_trace", - Title: "Get Trace", - Description: "Return the full causal tree for a trace. Each span is one execution row; spans are ordered by started_at ascending so the root is first. Use this after list_traces or after spotting a slow request.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in GetTraceInput) (*mcpsdk.CallToolResult, GetTraceOutput, error) { - if in.TraceID == "" { - return nil, GetTraceOutput{}, fmt.Errorf("trace_id required") - } - execs, err := deps.DB.ListByTraceID(in.TraceID) - if err != nil { - return nil, GetTraceOutput{}, fmt.Errorf("list trace: %w", err) - } - if len(execs) == 0 { - return nil, GetTraceOutput{}, fmt.Errorf("no spans found for trace %s", in.TraceID) - } - view := handlers.BuildTraceViewForMCP(in.TraceID, execs, deps.Registry) - out := GetTraceOutput{ - TraceID: view.TraceID, - RootSpanID: view.RootSpanID, - RootFunctionID: view.RootFunctionID, - Trigger: view.Trigger, - StartedAt: view.StartedAt, - TotalDurationMS: view.TotalDurationMS, - Status: view.Status, - HasOutlier: view.HasOutlier, - SpanCount: view.SpanCount, - Spans: make([]SpanRow, len(view.Spans)), - } - for i, sp := range view.Spans { - out.Spans[i] = SpanRow{ - ExecutionID: sp.ExecutionID, - SpanID: sp.SpanID, - ParentSpanID: sp.ParentSpanID, - FunctionID: sp.FunctionID, - FunctionName: sp.FunctionName, - ParentFunctionID: sp.ParentFunctionID, - Trigger: sp.Trigger, - Status: sp.Status, - StatusCode: sp.StatusCode, - ColdStart: sp.ColdStart, - IsOutlier: sp.IsOutlier, - BaselineP95MS: sp.BaselineP95MS, - StartedAt: sp.StartedAt, - DurationMS: sp.DurationMS, - OffsetMS: sp.OffsetMS, - ErrorMessage: sp.ErrorMessage, - } - } - // v0.6 SDK upgrade: include user-defined spans + structured - // log entries so MCP clients see the full picture, not - // just the system spans. - out.UserSpans = []UserSpanRow{} - if userSpans, err := deps.DB.ListUserSpansByTrace(in.TraceID); err == nil { - for _, us := range userSpans { - out.UserSpans = append(out.UserSpans, UserSpanRow{ - ID: us.ID, - ExecutionID: us.ExecutionID, - ParentSpanID: us.ParentSpanID, - Name: us.Name, - StartedAt: us.StartedAt.Format("2006-01-02T15:04:05.999999999Z"), - DurationMS: us.DurationMS, - OffsetMS: us.OffsetMS, - Status: us.Status, - ErrorMessage: us.ErrorMessage, - Attributes: us.Attributes, - }) - } - } - out.LogEntries = []LogEntryRow{} - if entries, err := deps.DB.ListLogEntriesByTrace(in.TraceID); err == nil { - for _, le := range entries { - out.LogEntries = append(out.LogEntries, LogEntryRow{ - ID: le.ID, - ExecutionID: le.ExecutionID, - SpanID: le.SpanID, - TS: le.TS.Format("2006-01-02T15:04:05.999999999Z"), - Level: le.Level, - Message: le.Message, - Fields: le.Fields, - }) - } - } - return nil, out, nil - }, - ) - }) +func registerTraceTools(rc *regCtx) { + deps := rc.deps + rc.group = "traces" - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "list_traces", - Title: "List Traces", - Description: "List recent root spans (one entry per trace). Filter by function, status, time range, or outlier flag. Pair with get_trace to drill into a specific causal chain.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in ListTracesInput) (*mcpsdk.CallToolResult, ListTracesOutput, error) { - params := database.ListRootSpansParams{ - FunctionID: resolveFnIDForBaseline(deps, in.FunctionID), - Status: in.Status, - OutlierOnly: in.OutlierOnly, - Since: in.Since, - Until: in.Until, - Limit: in.Limit, - } - if params.Limit <= 0 || params.Limit > 200 { - params.Limit = 50 + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "get_trace", + Title: "Get Trace", + Description: "Return the full causal tree for a trace. Each span is one execution row; spans are ordered by started_at ascending so the root is first. Use this after list_traces or after spotting a slow request.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in GetTraceInput) (*mcpsdk.CallToolResult, GetTraceOutput, error) { + if in.TraceID == "" { + return nil, GetTraceOutput{}, fmt.Errorf("trace_id required") + } + execs, err := deps.DB.ListByTraceID(in.TraceID) + if err != nil { + return nil, GetTraceOutput{}, fmt.Errorf("list trace: %w", err) + } + if len(execs) == 0 { + return nil, GetTraceOutput{}, fmt.Errorf("no spans found for trace %s", in.TraceID) + } + view := handlers.BuildTraceViewForMCP(in.TraceID, execs, deps.Registry) + out := GetTraceOutput{ + TraceID: view.TraceID, + RootSpanID: view.RootSpanID, + RootFunctionID: view.RootFunctionID, + Trigger: view.Trigger, + StartedAt: view.StartedAt, + TotalDurationMS: view.TotalDurationMS, + Status: view.Status, + HasOutlier: view.HasOutlier, + SpanCount: view.SpanCount, + Spans: make([]SpanRow, len(view.Spans)), + } + for i, sp := range view.Spans { + out.Spans[i] = SpanRow{ + ExecutionID: sp.ExecutionID, + SpanID: sp.SpanID, + ParentSpanID: sp.ParentSpanID, + FunctionID: sp.FunctionID, + FunctionName: sp.FunctionName, + ParentFunctionID: sp.ParentFunctionID, + Trigger: sp.Trigger, + Status: sp.Status, + StatusCode: sp.StatusCode, + ColdStart: sp.ColdStart, + IsOutlier: sp.IsOutlier, + BaselineP95MS: sp.BaselineP95MS, + StartedAt: sp.StartedAt, + DurationMS: sp.DurationMS, + OffsetMS: sp.OffsetMS, + ErrorMessage: sp.ErrorMessage, } - roots, err := deps.DB.ListRootSpans(params) - if err != nil { - return nil, ListTracesOutput{}, fmt.Errorf("list traces: %w", err) + } + // v0.6 SDK upgrade: include user-defined spans + structured + // log entries so MCP clients see the full picture, not + // just the system spans. + out.UserSpans = []UserSpanRow{} + if userSpans, err := deps.DB.ListUserSpansByTrace(in.TraceID); err == nil { + for _, us := range userSpans { + out.UserSpans = append(out.UserSpans, UserSpanRow{ + ID: us.ID, + ExecutionID: us.ExecutionID, + ParentSpanID: us.ParentSpanID, + Name: us.Name, + StartedAt: us.StartedAt.Format("2006-01-02T15:04:05.999999999Z"), + DurationMS: us.DurationMS, + OffsetMS: us.OffsetMS, + Status: us.Status, + ErrorMessage: us.ErrorMessage, + Attributes: us.Attributes, + }) } - rows := make([]RootSpanRow, 0, len(roots)) - for _, e := range roots { - var name string - if deps.Registry != nil { - if fn, err := deps.Registry.Get(e.FunctionID); err == nil { - name = fn.Name - } - } - var dur int64 - if e.DurationMS != nil { - dur = *e.DurationMS - } - var sc int - if e.StatusCode != nil { - sc = *e.StatusCode - } - rows = append(rows, RootSpanRow{ - TraceID: e.TraceID, - RootSpanID: e.SpanID, - RootFunctionID: e.FunctionID, - FunctionName: name, - Trigger: e.Trigger, - StartedAt: e.StartedAt.Format("2006-01-02T15:04:05.000Z07:00"), - DurationMS: dur, - Status: e.Status, - StatusCode: sc, - IsOutlier: e.IsOutlier, + } + out.LogEntries = []LogEntryRow{} + if entries, err := deps.DB.ListLogEntriesByTrace(in.TraceID); err == nil { + for _, le := range entries { + out.LogEntries = append(out.LogEntries, LogEntryRow{ + ID: le.ID, + ExecutionID: le.ExecutionID, + SpanID: le.SpanID, + TS: le.TS.Format("2006-01-02T15:04:05.999999999Z"), + Level: le.Level, + Message: le.Message, + Fields: le.Fields, }) } - return nil, ListTracesOutput{Traces: rows, Count: len(rows)}, nil - }, - ) - }) + } + return nil, out, nil + }, + ) - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "get_function_baseline", - Title: "Get Function Baseline", - Description: "Return the rolling P95/P99/mean latency baseline for a function plus the current sample count. baseline drives the outlier flag on each execution.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in GetFunctionBaselineInput) (*mcpsdk.CallToolResult, metrics.BaselineSummary, error) { - if in.FunctionID == "" { - return nil, metrics.BaselineSummary{}, fmt.Errorf("function_id required") + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "list_traces", + Title: "List Traces", + Description: "List recent root spans (one entry per trace). Filter by function, status, time range, or outlier flag. Pair with get_trace to drill into a specific causal chain.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in ListTracesInput) (*mcpsdk.CallToolResult, ListTracesOutput, error) { + params := database.ListRootSpansParams{ + FunctionID: resolveFnIDForBaseline(deps, in.FunctionID), + Status: in.Status, + OutlierOnly: in.OutlierOnly, + Since: in.Since, + Until: in.Until, + Limit: in.Limit, + } + if params.Limit <= 0 || params.Limit > 200 { + params.Limit = 50 + } + roots, err := deps.DB.ListRootSpans(params) + if err != nil { + return nil, ListTracesOutput{}, fmt.Errorf("list traces: %w", err) + } + rows := make([]RootSpanRow, 0, len(roots)) + for _, e := range roots { + var name string + if deps.Registry != nil { + if fn, err := deps.Registry.Get(e.FunctionID); err == nil { + name = fn.Name + } } - fnID := resolveFnIDForBaseline(deps, in.FunctionID) - if deps.Metrics == nil { - return nil, metrics.BaselineSummary{FunctionID: fnID, WindowSize: 100}, nil + var dur int64 + if e.DurationMS != nil { + dur = *e.DurationMS } - return nil, deps.Metrics.Baselines.Summary(fnID), nil - }, - ) - }) + var sc int + if e.StatusCode != nil { + sc = *e.StatusCode + } + rows = append(rows, RootSpanRow{ + TraceID: e.TraceID, + RootSpanID: e.SpanID, + RootFunctionID: e.FunctionID, + FunctionName: name, + Trigger: e.Trigger, + StartedAt: e.StartedAt.Format("2006-01-02T15:04:05.000Z07:00"), + DurationMS: dur, + Status: e.Status, + StatusCode: sc, + IsOutlier: e.IsOutlier, + }) + } + return nil, ListTracesOutput{Traces: rows, Count: len(rows)}, nil + }, + ) + + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "get_function_baseline", + Title: "Get Function Baseline", + Description: "Return the rolling P95/P99/mean latency baseline for a function plus the current sample count. baseline drives the outlier flag on each execution.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in GetFunctionBaselineInput) (*mcpsdk.CallToolResult, metrics.BaselineSummary, error) { + if in.FunctionID == "" { + return nil, metrics.BaselineSummary{}, fmt.Errorf("function_id required") + } + fnID := resolveFnIDForBaseline(deps, in.FunctionID) + if deps.Metrics == nil { + return nil, metrics.BaselineSummary{FunctionID: fnID, WindowSize: 100}, nil + } + return nil, deps.Metrics.Baselines.Summary(fnID), nil + }, + ) } // resolveFnIDForBaseline accepts either fn_xxx or a friendly name and diff --git a/backend/internal/mcp/tools_webhooks.go b/backend/internal/mcp/tools_webhooks.go index d11e261..3e9cd94 100644 --- a/backend/internal/mcp/tools_webhooks.go +++ b/backend/internal/mcp/tools_webhooks.go @@ -146,213 +146,201 @@ type ListDeliveriesOutput struct { Deliveries []DeliveryView `json:"deliveries"` } -func registerWebhookTools(s *mcpsdk.Server, deps Deps, perms permSet) { - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "list_webhooks", - Title: "List Webhooks", - Description: "List every operator-configured webhook subscription. The plaintext secret is never returned (only the preview); use create_webhook to mint a new one if you've lost it.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, ListWebhooksOutput, error) { - rows, err := deps.DB.ListEventSubscriptions() - if err != nil { - return nil, ListWebhooksOutput{}, err - } - out := ListWebhooksOutput{Subscriptions: make([]SubscriptionView, 0, len(rows))} - for _, r := range rows { - out.Subscriptions = append(out.Subscriptions, toSubscriptionView(r)) - } - return nil, out, nil - }, - ) - }) +func registerWebhookTools(rc *regCtx) { + deps := rc.deps + rc.group = "webhooks" + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "list_webhooks", + Title: "List Webhooks", + Description: "List every operator-configured webhook subscription. The plaintext secret is never returned (only the preview); use create_webhook to mint a new one if you've lost it.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, ListWebhooksOutput, error) { + rows, err := deps.DB.ListEventSubscriptions() + if err != nil { + return nil, ListWebhooksOutput{}, err + } + out := ListWebhooksOutput{Subscriptions: make([]SubscriptionView, 0, len(rows))} + for _, r := range rows { + out.Subscriptions = append(out.Subscriptions, toSubscriptionView(r)) + } + return nil, out, nil + }, + ) - gatedAdd(perms, permAdmin, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "create_webhook", - Title: "Create Webhook", - Description: "Create a new webhook subscription. Returns the subscription record AND the plaintext HMAC secret — the secret is shown ONLY once; capture it now and configure your receiver to verify X-Orva-Signature.", - Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: false, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in CreateWebhookInput) (*mcpsdk.CallToolResult, CreateWebhookOutput, error) { - name := strings.TrimSpace(in.Name) - if name == "" { - return nil, CreateWebhookOutput{}, errors.New("name is required") - } - if err := validateURL(in.URL); err != nil { - return nil, CreateWebhookOutput{}, err + regAddTool(rc, permAdmin, + &mcpsdk.Tool{ + Name: "create_webhook", + Title: "Create Webhook", + Description: "Create a new webhook subscription. Returns the subscription record AND the plaintext HMAC secret — the secret is shown ONLY once; capture it now and configure your receiver to verify X-Orva-Signature.", + Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: false, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in CreateWebhookInput) (*mcpsdk.CallToolResult, CreateWebhookOutput, error) { + name := strings.TrimSpace(in.Name) + if name == "" { + return nil, CreateWebhookOutput{}, errors.New("name is required") + } + if err := validateURL(in.URL); err != nil { + return nil, CreateWebhookOutput{}, err + } + events := normalizeAgentEvents(in.Events) + if err := validateAgentEvents(events); err != nil { + return nil, CreateWebhookOutput{}, err + } + enabled := true + if in.Enabled != nil { + enabled = *in.Enabled + } + secret := database.NewWebhookSecret() + sub := &database.EventSubscription{ + Name: name, + URL: strings.TrimSpace(in.URL), + Secret: secret, + Events: events, + Enabled: enabled, + } + if err := deps.DB.InsertEventSubscription(sub); err != nil { + return nil, CreateWebhookOutput{}, err + } + sub.SecretPreview = secret[:8] + "…" + return nil, CreateWebhookOutput{Subscription: toSubscriptionView(sub), Secret: secret}, nil + }, + ) + + regAddTool(rc, permAdmin, + &mcpsdk.Tool{ + Name: "update_webhook", + Title: "Update Webhook", + Description: "Edit an existing subscription. Any of name / url / events / enabled may be supplied; omitted fields keep their previous values. Secret cannot be rotated through this path — delete and re-create to rotate.", + Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in UpdateWebhookInput) (*mcpsdk.CallToolResult, SubscriptionView, error) { + sub, err := deps.DB.GetEventSubscription(in.ID) + if err != nil { + return nil, SubscriptionView{}, errors.New("webhook not found") + } + if name := strings.TrimSpace(in.Name); name != "" { + sub.Name = name + } + if u := strings.TrimSpace(in.URL); u != "" { + if err := validateURL(u); err != nil { + return nil, SubscriptionView{}, err } + sub.URL = u + } + if in.Events != nil { events := normalizeAgentEvents(in.Events) if err := validateAgentEvents(events); err != nil { - return nil, CreateWebhookOutput{}, err - } - enabled := true - if in.Enabled != nil { - enabled = *in.Enabled - } - secret := database.NewWebhookSecret() - sub := &database.EventSubscription{ - Name: name, - URL: strings.TrimSpace(in.URL), - Secret: secret, - Events: events, - Enabled: enabled, - } - if err := deps.DB.InsertEventSubscription(sub); err != nil { - return nil, CreateWebhookOutput{}, err - } - sub.SecretPreview = secret[:8] + "…" - return nil, CreateWebhookOutput{Subscription: toSubscriptionView(sub), Secret: secret}, nil - }, - ) - }) - - gatedAdd(perms, permAdmin, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "update_webhook", - Title: "Update Webhook", - Description: "Edit an existing subscription. Any of name / url / events / enabled may be supplied; omitted fields keep their previous values. Secret cannot be rotated through this path — delete and re-create to rotate.", - Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in UpdateWebhookInput) (*mcpsdk.CallToolResult, SubscriptionView, error) { - sub, err := deps.DB.GetEventSubscription(in.ID) - if err != nil { - return nil, SubscriptionView{}, errors.New("webhook not found") - } - if name := strings.TrimSpace(in.Name); name != "" { - sub.Name = name - } - if u := strings.TrimSpace(in.URL); u != "" { - if err := validateURL(u); err != nil { - return nil, SubscriptionView{}, err - } - sub.URL = u - } - if in.Events != nil { - events := normalizeAgentEvents(in.Events) - if err := validateAgentEvents(events); err != nil { - return nil, SubscriptionView{}, err - } - sub.Events = events - } - if in.Enabled != nil { - sub.Enabled = *in.Enabled - } - if err := deps.DB.UpdateEventSubscription(sub); err != nil { return nil, SubscriptionView{}, err } - return nil, toSubscriptionView(sub), nil - }, - ) - }) + sub.Events = events + } + if in.Enabled != nil { + sub.Enabled = *in.Enabled + } + if err := deps.DB.UpdateEventSubscription(sub); err != nil { + return nil, SubscriptionView{}, err + } + return nil, toSubscriptionView(sub), nil + }, + ) - gatedAdd(perms, permAdmin, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "delete_webhook", - Title: "Delete Webhook", - Description: "Remove a webhook subscription. Pass confirm=true. All in-flight deliveries are removed via FK cascade.", - Annotations: &mcpsdk.ToolAnnotations{DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in WebhookDeleteInput) (*mcpsdk.CallToolResult, WebhookOpOutput, error) { - if !in.Confirm { - return nil, WebhookOpOutput{}, errors.New("delete refused: pass confirm=true") - } - if _, err := deps.DB.GetEventSubscription(in.ID); err != nil { - return nil, WebhookOpOutput{}, errors.New("webhook not found") - } - if err := deps.DB.DeleteEventSubscription(in.ID); err != nil { - return nil, WebhookOpOutput{}, err - } - return nil, WebhookOpOutput{ID: in.ID, Status: "deleted"}, nil - }, - ) - }) + regAddTool(rc, permAdmin, + &mcpsdk.Tool{ + Name: "delete_webhook", + Title: "Delete Webhook", + Description: "Remove a webhook subscription. Pass confirm=true. All in-flight deliveries are removed via FK cascade.", + Annotations: &mcpsdk.ToolAnnotations{DestructiveHint: ptrTrue(), OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in WebhookDeleteInput) (*mcpsdk.CallToolResult, WebhookOpOutput, error) { + if !in.Confirm { + return nil, WebhookOpOutput{}, errors.New("delete refused: pass confirm=true") + } + if _, err := deps.DB.GetEventSubscription(in.ID); err != nil { + return nil, WebhookOpOutput{}, errors.New("webhook not found") + } + if err := deps.DB.DeleteEventSubscription(in.ID); err != nil { + return nil, WebhookOpOutput{}, err + } + return nil, WebhookOpOutput{ID: in.ID, Status: "deleted"}, nil + }, + ) - gatedAdd(perms, permAdmin, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "test_webhook", - Title: "Test Webhook", - Description: "Queue a synthetic 'webhook.test' delivery against a subscription's URL so you can validate signature handling without waiting for a real event. Picked up on the next 5s scheduler tick.", - Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: false, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in WebhookIDInput) (*mcpsdk.CallToolResult, WebhookOpOutput, error) { - sub, err := deps.DB.GetEventSubscription(in.ID) - if err != nil { - return nil, WebhookOpOutput{}, errors.New("webhook not found") - } - envelope := map[string]any{ - "id": "evt_test_" + database.NewSubscriptionID()[4:], - "type": "webhook.test", - "fired_at": time.Now().UTC().Format(time.RFC3339Nano), - "data": map[string]any{ - "subscription_id": sub.ID, - "name": sub.Name, - "message": "synthetic test event from MCP test_webhook", - }, - } - body, _ := json.Marshal(envelope) - d := &database.WebhookDelivery{ - SubscriptionID: sub.ID, - EventName: "webhook.test", - Payload: body, - Status: "pending", - MaxAttempts: 1, - } - if err := deps.DB.InsertDelivery(d); err != nil { - return nil, WebhookOpOutput{}, err - } - return nil, WebhookOpOutput{ID: d.ID, Status: "queued"}, nil - }, - ) - }) + regAddTool(rc, permAdmin, + &mcpsdk.Tool{ + Name: "test_webhook", + Title: "Test Webhook", + Description: "Queue a synthetic 'webhook.test' delivery against a subscription's URL so you can validate signature handling without waiting for a real event. Picked up on the next 5s scheduler tick.", + Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: false, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in WebhookIDInput) (*mcpsdk.CallToolResult, WebhookOpOutput, error) { + sub, err := deps.DB.GetEventSubscription(in.ID) + if err != nil { + return nil, WebhookOpOutput{}, errors.New("webhook not found") + } + envelope := map[string]any{ + "id": "evt_test_" + database.NewSubscriptionID()[4:], + "type": "webhook.test", + "fired_at": time.Now().UTC().Format(time.RFC3339Nano), + "data": map[string]any{ + "subscription_id": sub.ID, + "name": sub.Name, + "message": "synthetic test event from MCP test_webhook", + }, + } + body, _ := json.Marshal(envelope) + d := &database.WebhookDelivery{ + SubscriptionID: sub.ID, + EventName: "webhook.test", + Payload: body, + Status: "pending", + MaxAttempts: 1, + } + if err := deps.DB.InsertDelivery(d); err != nil { + return nil, WebhookOpOutput{}, err + } + return nil, WebhookOpOutput{ID: d.ID, Status: "queued"}, nil + }, + ) - gatedAdd(perms, permRead, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "list_webhook_deliveries", - Title: "List Webhook Deliveries", - Description: "List recent deliveries for a webhook subscription. Useful for diagnosing stuck retries or confirming a fire happened.", - Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in ListDeliveriesInput) (*mcpsdk.CallToolResult, ListDeliveriesOutput, error) { - rows, err := deps.DB.ListDeliveriesForSubscription(in.ID, in.Limit) - if err != nil { - return nil, ListDeliveriesOutput{}, err - } - out := ListDeliveriesOutput{Deliveries: make([]DeliveryView, 0, len(rows))} - for _, d := range rows { - out.Deliveries = append(out.Deliveries, toDeliveryView(d)) - } - return nil, out, nil - }, - ) - }) + regAddTool(rc, permRead, + &mcpsdk.Tool{ + Name: "list_webhook_deliveries", + Title: "List Webhook Deliveries", + Description: "List recent deliveries for a webhook subscription. Useful for diagnosing stuck retries or confirming a fire happened.", + Annotations: &mcpsdk.ToolAnnotations{ReadOnlyHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in ListDeliveriesInput) (*mcpsdk.CallToolResult, ListDeliveriesOutput, error) { + rows, err := deps.DB.ListDeliveriesForSubscription(in.ID, in.Limit) + if err != nil { + return nil, ListDeliveriesOutput{}, err + } + out := ListDeliveriesOutput{Deliveries: make([]DeliveryView, 0, len(rows))} + for _, d := range rows { + out.Deliveries = append(out.Deliveries, toDeliveryView(d)) + } + return nil, out, nil + }, + ) - gatedAdd(perms, permAdmin, func() { - mcpsdk.AddTool(s, - &mcpsdk.Tool{ - Name: "retry_webhook_delivery", - Title: "Retry Webhook Delivery", - Description: "Reset a terminal (failed) delivery back to pending so the scheduler will re-attempt it. attempts is reset to 0.", - Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, OpenWorldHint: ptrFalse()}, - }, - func(_ context.Context, _ *mcpsdk.CallToolRequest, in WebhookIDInput) (*mcpsdk.CallToolResult, WebhookOpOutput, error) { - if _, err := deps.DB.GetDelivery(in.ID); err != nil { - return nil, WebhookOpOutput{}, errors.New("delivery not found") - } - if err := deps.DB.RetryDelivery(in.ID); err != nil { - return nil, WebhookOpOutput{}, err - } - return nil, WebhookOpOutput{ID: in.ID, Status: "pending"}, nil - }, - ) - }) + regAddTool(rc, permAdmin, + &mcpsdk.Tool{ + Name: "retry_webhook_delivery", + Title: "Retry Webhook Delivery", + Description: "Reset a terminal (failed) delivery back to pending so the scheduler will re-attempt it. attempts is reset to 0.", + Annotations: &mcpsdk.ToolAnnotations{IdempotentHint: true, OpenWorldHint: ptrFalse()}, + }, + func(_ context.Context, _ *mcpsdk.CallToolRequest, in WebhookIDInput) (*mcpsdk.CallToolResult, WebhookOpOutput, error) { + if _, err := deps.DB.GetDelivery(in.ID); err != nil { + return nil, WebhookOpOutput{}, errors.New("delivery not found") + } + if err := deps.DB.RetryDelivery(in.ID); err != nil { + return nil, WebhookOpOutput{}, err + } + return nil, WebhookOpOutput{ID: in.ID, Status: "pending"}, nil + }, + ) } // ── helpers ──────────────────────────────────────────────────────── diff --git a/backend/internal/secrets/secrets.go b/backend/internal/secrets/secrets.go index b84bfbd..37e247a 100644 --- a/backend/internal/secrets/secrets.go +++ b/backend/internal/secrets/secrets.go @@ -124,6 +124,20 @@ func (m *Manager) decrypt(b64 string) (string, error) { return string(pt), nil } +// EncryptValue encrypts an arbitrary string with the same master key and +// AES-256-GCM cipher used for function secrets, returning base64(nonce || +// ciphertext). It exists so other subsystems (e.g. the AI chat agent's +// provider API keys in ai_provider_configs) can reuse the existing +// at-rest crypto without piggy-backing on the per-function secrets table. +func (m *Manager) EncryptValue(plaintext string) (string, error) { + return m.encrypt(plaintext) +} + +// DecryptValue reverses EncryptValue. +func (m *Manager) DecryptValue(b64 string) (string, error) { + return m.decrypt(b64) +} + // Upsert stores (or overwrites) a secret for a function. func (m *Manager) Upsert(functionID, key, value string) error { if key == "" { diff --git a/backend/internal/server/ai_handler.go b/backend/internal/server/ai_handler.go new file mode 100644 index 0000000..ed1e407 --- /dev/null +++ b/backend/internal/server/ai_handler.go @@ -0,0 +1,378 @@ +package server + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log/slog" + "net/http" + "strconv" + "sync" + "time" + + "github.com/Harsh-2002/Orva/backend/internal/ai" + "github.com/Harsh-2002/Orva/backend/internal/auth" + "github.com/Harsh-2002/Orva/backend/internal/database" + "github.com/Harsh-2002/Orva/backend/internal/server/handlers/respond" +) + +// AIHandler serves /api/v1/ai/* — the in-product AI chat assistant. Chat and +// tool-approval stream Server-Sent Events; everything else is JSON. It lives in +// package server (not handlers) because it needs ActorFromContext for the +// caller's identity + permissions, which scope the agent's tool catalog. +type AIHandler struct { + Manager *ai.Manager + DB *database.Database +} + +// ─── identity ──────────────────────────────────────────────────────────────── + +// principal resolves the caller into an ai.Principal. Web sessions are the +// operator and get full access; API keys are scoped to their permission set. +func (h *AIHandler) principal(r *http.Request) ai.Principal { + a := ActorFromContext(r.Context()) + if a == nil { + return ai.Principal{Perms: auth.PermSet{}} + } + p := ai.Principal{ID: a.ID, Label: a.Label} + if a.Type == "api_key" { + ps := auth.PermSet{} + if k, err := h.DB.GetAPIKeyByID(a.ID); err == nil { + for _, perm := range k.PermissionsList() { + ps[perm] = true + } + } + p.Perms = ps + return p + } + // session / internal operator → full access (the dashboard is the operator console). + p.Perms = auth.PermSet{"read": true, "write": true, "invoke": true, "admin": true} + return p +} + +// ─── SSE sink ──────────────────────────────────────────────────────────────── + +type sseSink struct { + w http.ResponseWriter + f http.Flusher + mu sync.Mutex +} + +func (s *sseSink) Send(event string, data any) error { + b, err := json.Marshal(data) + if err != nil { + b = []byte(`{}`) + } + s.mu.Lock() + defer s.mu.Unlock() + if _, err := fmt.Fprintf(s.w, "event: %s\ndata: %s\n\n", event, b); err != nil { + return err + } + s.f.Flush() + return nil +} + +// Ping writes an SSE comment frame to keep the connection alive during long +// pre-token model "thinking" gaps. Comment frames (lines not starting with +// event:/data:) are ignored by the client parser, so they're invisible to the UI. +func (s *sseSink) Ping() { + s.mu.Lock() + defer s.mu.Unlock() + if _, err := fmt.Fprint(s.w, ": ping\n\n"); err == nil { + s.f.Flush() + } +} + +// heartbeat pings the sink every 15s until the returned stop() is called. Safe +// to run concurrently with Send (both take the sink mutex). +func heartbeat(sink *sseSink) (stop func()) { + done := make(chan struct{}) + go func() { + t := time.NewTicker(15 * time.Second) + defer t.Stop() + for { + select { + case <-done: + return + case <-t.C: + sink.Ping() + } + } + }() + return func() { close(done) } +} + +func startSSE(w http.ResponseWriter) (*sseSink, bool) { + f, ok := w.(http.Flusher) + if !ok { + return nil, false + } + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("X-Accel-Buffering", "no") + f.Flush() + return &sseSink{w: w, f: f}, true +} + +// ─── chat (SSE) ────────────────────────────────────────────────────────────── + +func (h *AIHandler) ChatStream(w http.ResponseWriter, r *http.Request) { + var body struct { + ConversationID string `json:"conversation_id"` + Content string `json:"content"` + Provider string `json:"provider"` + Model string `json:"model"` + Thinking string `json:"thinking"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + respond.Error(w, http.StatusBadRequest, "BAD_REQUEST", "invalid JSON body", RequestID(r.Context())) + return + } + if body.Content == "" { + respond.Error(w, http.StatusBadRequest, "BAD_REQUEST", "content is required", RequestID(r.Context())) + return + } + sink, ok := startSSE(w) + if !ok { + respond.Error(w, http.StatusInternalServerError, "NO_STREAM", "streaming unsupported", RequestID(r.Context())) + return + } + defer heartbeat(sink)() + // Errors are streamed as SSE `error` frames by the ai layer (it owns the + // sink); the returned error is logged for operator visibility. + _, err := h.Manager.Chat(r.Context(), sink, h.principal(r), ai.ChatParams{ + ConversationID: body.ConversationID, + Content: body.Content, + Provider: body.Provider, + Model: body.Model, + Thinking: body.Thinking, + }) + logAIError(r, "chat", err) +} + +// logAIError surfaces a manager-returned error in the server log. Expected, +// non-actionable conditions (client disconnect, conversation-busy) are skipped +// so the log stays signal. +func logAIError(r *http.Request, op string, err error) { + if err == nil || errors.Is(err, context.Canceled) || errors.Is(err, ai.ErrConversationBusy) { + return + } + slog.Warn("ai "+op+" failed", "error", err, "request_id", RequestID(r.Context())) +} + +// ─── tool approval (SSE — resumes the paused turn) ─────────────────────────── + +func (h *AIHandler) ApproveTool(w http.ResponseWriter, r *http.Request) { h.resume(w, r, true) } +func (h *AIHandler) RejectTool(w http.ResponseWriter, r *http.Request) { h.resume(w, r, false) } + +func (h *AIHandler) resume(w http.ResponseWriter, r *http.Request, approved bool) { + id := r.PathValue("id") + tc, err := h.Manager.GetToolCall(id) + if err != nil { + respond.Error(w, http.StatusNotFound, "NOT_FOUND", "tool call not found", RequestID(r.Context())) + return + } + sink, ok := startSSE(w) + if !ok { + respond.Error(w, http.StatusInternalServerError, "NO_STREAM", "streaming unsupported", RequestID(r.Context())) + return + } + defer heartbeat(sink)() + // As with chat, the ai layer streams any error as an SSE `error` frame. + logAIError(r, "resume", h.Manager.Resume(r.Context(), sink, h.principal(r), tc.ConversationID, id, approved)) +} + +// ─── conversations (JSON) ──────────────────────────────────────────────────── + +func (h *AIHandler) ListConversations(w http.ResponseWriter, r *http.Request) { + archived := r.URL.Query().Get("archived") == "true" + limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) + offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) + // Single-operator instance: list all conversations (see manager.go). + convs, err := h.Manager.ListConversations("", archived, limit, offset) + if err != nil { + respond.Error(w, http.StatusInternalServerError, "DB_ERROR", err.Error(), RequestID(r.Context())) + return + } + respond.JSON(w, http.StatusOK, map[string]any{"conversations": convs}) +} + +func (h *AIHandler) CreateConversation(w http.ResponseWriter, r *http.Request) { + var body struct { + Title string `json:"title"` + } + _ = json.NewDecoder(r.Body).Decode(&body) + c, err := h.Manager.CreateConversation(h.principal(r).ID, body.Title) + if err != nil { + respond.Error(w, http.StatusInternalServerError, "DB_ERROR", err.Error(), RequestID(r.Context())) + return + } + respond.JSON(w, http.StatusOK, map[string]any{"conversation": c}) +} + +func (h *AIHandler) GetConversation(w http.ResponseWriter, r *http.Request) { + detail, err := h.Manager.GetConversation(r.PathValue("id")) + if err != nil { + respond.Error(w, http.StatusNotFound, "NOT_FOUND", "conversation not found", RequestID(r.Context())) + return + } + respond.JSON(w, http.StatusOK, detail) +} + +func (h *AIHandler) PatchConversation(w http.ResponseWriter, r *http.Request) { + var body struct { + Title *string `json:"title"` + Archived *bool `json:"archived"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + respond.Error(w, http.StatusBadRequest, "BAD_REQUEST", "invalid JSON body", RequestID(r.Context())) + return + } + c, err := h.Manager.UpdateConversation(r.PathValue("id"), body.Title, body.Archived) + if err != nil { + respond.Error(w, http.StatusNotFound, "NOT_FOUND", err.Error(), RequestID(r.Context())) + return + } + respond.JSON(w, http.StatusOK, map[string]any{"conversation": c}) +} + +func (h *AIHandler) DeleteConversation(w http.ResponseWriter, r *http.Request) { + if err := h.Manager.DeleteConversation(r.PathValue("id")); err != nil { + respond.Error(w, http.StatusInternalServerError, "DB_ERROR", err.Error(), RequestID(r.Context())) + return + } + w.WriteHeader(http.StatusNoContent) +} + +func (h *AIHandler) ListMessages(w http.ResponseWriter, r *http.Request) { + since, _ := strconv.Atoi(r.URL.Query().Get("since_seq")) + msgs, err := h.Manager.ListMessages(r.PathValue("id"), since) + if err != nil { + respond.Error(w, http.StatusInternalServerError, "DB_ERROR", err.Error(), RequestID(r.Context())) + return + } + respond.JSON(w, http.StatusOK, map[string]any{"messages": msgs}) +} + +// Regenerate (SSE) drops the last assistant turn and re-runs the last user +// message for a fresh answer. +func (h *AIHandler) Regenerate(w http.ResponseWriter, r *http.Request) { + var body struct { + Provider string `json:"provider"` + Model string `json:"model"` + Thinking string `json:"thinking"` + } + _ = json.NewDecoder(r.Body).Decode(&body) + sink, ok := startSSE(w) + if !ok { + respond.Error(w, http.StatusInternalServerError, "NO_STREAM", "streaming unsupported", RequestID(r.Context())) + return + } + defer heartbeat(sink)() + logAIError(r, "regenerate", h.Manager.RegenerateLast(r.Context(), sink, h.principal(r), r.PathValue("id"), + ai.ChatOverrides{Provider: body.Provider, Model: body.Model, Thinking: body.Thinking})) +} + +// EditMessage (SSE) rewrites a user message, truncates from it, and re-runs. +func (h *AIHandler) EditMessage(w http.ResponseWriter, r *http.Request) { + var body struct { + Content string `json:"content"` + Provider string `json:"provider"` + Model string `json:"model"` + Thinking string `json:"thinking"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Content == "" { + respond.Error(w, http.StatusBadRequest, "BAD_REQUEST", "content is required", RequestID(r.Context())) + return + } + sink, ok := startSSE(w) + if !ok { + respond.Error(w, http.StatusInternalServerError, "NO_STREAM", "streaming unsupported", RequestID(r.Context())) + return + } + defer heartbeat(sink)() + logAIError(r, "edit", h.Manager.EditAndResend(r.Context(), sink, h.principal(r), r.PathValue("id"), r.PathValue("mid"), body.Content, + ai.ChatOverrides{Provider: body.Provider, Model: body.Model, Thinking: body.Thinking})) +} + +// DeleteMessage truncates a conversation from a message onward (JSON, no re-run). +func (h *AIHandler) DeleteMessage(w http.ResponseWriter, r *http.Request) { + if err := h.Manager.DeleteMessageFrom(r.PathValue("id"), r.PathValue("mid")); err != nil { + if errors.Is(err, ai.ErrConversationBusy) { + respond.Error(w, http.StatusConflict, "CONVERSATION_BUSY", err.Error(), RequestID(r.Context())) + return + } + respond.Error(w, http.StatusNotFound, "NOT_FOUND", err.Error(), RequestID(r.Context())) + return + } + w.WriteHeader(http.StatusNoContent) +} + +// ─── providers (JSON) ──────────────────────────────────────────────────────── + +func (h *AIHandler) ListProviders(w http.ResponseWriter, r *http.Request) { + ps, err := h.Manager.ListProviders() + if err != nil { + respond.Error(w, http.StatusInternalServerError, "DB_ERROR", err.Error(), RequestID(r.Context())) + return + } + respond.JSON(w, http.StatusOK, map[string]any{"providers": ps}) +} + +func (h *AIHandler) SaveProvider(w http.ResponseWriter, r *http.Request) { + var in ai.ProviderInput + if err := json.NewDecoder(r.Body).Decode(&in); err != nil { + respond.Error(w, http.StatusBadRequest, "BAD_REQUEST", "invalid JSON body", RequestID(r.Context())) + return + } + v, err := h.Manager.SaveProvider(in) + if err != nil { + respond.Error(w, http.StatusBadRequest, "BAD_REQUEST", err.Error(), RequestID(r.Context())) + return + } + respond.JSON(w, http.StatusOK, map[string]any{"provider": v}) +} + +func (h *AIHandler) DeleteProvider(w http.ResponseWriter, r *http.Request) { + if err := h.Manager.DeleteProvider(r.PathValue("id")); err != nil { + respond.Error(w, http.StatusInternalServerError, "DB_ERROR", err.Error(), RequestID(r.Context())) + return + } + w.WriteHeader(http.StatusNoContent) +} + +// ─── models + settings (JSON) ──────────────────────────────────────────────── + +// ListProviderModels dynamically lists the models a configured provider exposes +// (queried live from its /v1/models endpoint). Returns 200 with {models, error?} +// — a non-empty error means the listing failed and the UI should let the user +// type a model id manually. +func (h *AIHandler) ListProviderModels(w http.ResponseWriter, r *http.Request) { + models, err := h.Manager.ProviderModels(r.PathValue("id")) + out := map[string]any{"models": models} + if err != nil { + out["models"] = []any{} + out["error"] = err.Error() + } + respond.JSON(w, http.StatusOK, out) +} + +func (h *AIHandler) GetSettings(w http.ResponseWriter, r *http.Request) { + respond.JSON(w, http.StatusOK, map[string]any{"settings": h.Manager.Settings()}) +} + +func (h *AIHandler) PutSettings(w http.ResponseWriter, r *http.Request) { + var in database.AISettings + if err := json.NewDecoder(r.Body).Decode(&in); err != nil { + respond.Error(w, http.StatusBadRequest, "BAD_REQUEST", "invalid JSON body", RequestID(r.Context())) + return + } + s, err := h.Manager.SaveSettings(in) + if err != nil { + respond.Error(w, http.StatusBadRequest, "BAD_REQUEST", err.Error(), RequestID(r.Context())) + return + } + respond.JSON(w, http.StatusOK, map[string]any{"settings": s}) +} diff --git a/backend/internal/server/handlers/keys.go b/backend/internal/server/handlers/keys.go index f3d3410..17aa93f 100644 --- a/backend/internal/server/handlers/keys.go +++ b/backend/internal/server/handlers/keys.go @@ -17,6 +17,10 @@ import ( // KeyHandler handles API key management endpoints. type KeyHandler struct { DB *database.Database + // InvalidateKey evicts a key (by its hash) from the auth middleware's + // in-memory cache so a deleted key stops authenticating immediately. + // Optional; nil is a no-op. + InvalidateKey func(keyHash string) } // createKeyRequest is the body for creating an API key. @@ -136,11 +140,22 @@ func (h *KeyHandler) Delete(w http.ResponseWriter, r *http.Request) { return } + // Capture the key's hash BEFORE deleting so we can evict it from the auth + // cache — otherwise a revoked key keeps working until process restart. + var keyHash string + if k, err := h.DB.GetAPIKeyByID(keyID); err == nil && k != nil { + keyHash = k.KeyHash + } + if err := h.DB.DeleteAPIKey(keyID); err != nil { respond.Error(w, http.StatusInternalServerError, "INTERNAL", "failed to delete API key", reqID) return } + if h.InvalidateKey != nil && keyHash != "" { + h.InvalidateKey(keyHash) + } + respond.JSON(w, http.StatusOK, map[string]string{"status": "deleted", "id": keyID}) } diff --git a/backend/internal/server/middleware_auth.go b/backend/internal/server/middleware_auth.go index 367bf50..a4f7ea2 100644 --- a/backend/internal/server/middleware_auth.go +++ b/backend/internal/server/middleware_auth.go @@ -23,9 +23,11 @@ type sessionCacheEntry struct { const sessionCacheTTL = 30 * time.Second // authMiddleware validates API key authentication and permission checks. -// Uses an in-memory cache to avoid hitting SQLite on every request. -func authMiddleware(db *database.Database, next http.Handler) http.Handler { - var keyCache sync.Map // keyHash -> *database.APIKey +// Uses an in-memory cache to avoid hitting SQLite on every request. keyCache is +// owned by the Router and shared so the keys handler can evict an entry the +// instant a key is deleted — otherwise a revoked key would keep authenticating +// until process restart (it is only otherwise dropped on expiry). +func authMiddleware(db *database.Database, keyCache *sync.Map, next http.Handler) http.Handler { var sessionCache sync.Map // token -> sessionCacheEntry return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -187,6 +189,12 @@ func requiredPermission(method, path string) string { if strings.HasPrefix(path, "/api/v1/keys") { return "admin" } + // The in-product AI assistant operates the whole instance via tool calls + // and stores provider keys; the entire surface is admin-only (the dashboard + // session resolves to admin). Conversations are a shared operator space. + if strings.HasPrefix(path, "/api/v1/ai/") { + return "admin" + } if path == "/api/v1/pool/config" && (method == http.MethodPut || method == http.MethodPost) { return "admin" } diff --git a/backend/internal/server/router.go b/backend/internal/server/router.go index 5bcad08..e44e420 100644 --- a/backend/internal/server/router.go +++ b/backend/internal/server/router.go @@ -3,8 +3,10 @@ package server import ( "net/http" "strings" + "sync" "time" + aipkg "github.com/Harsh-2002/Orva/backend/internal/ai" "github.com/Harsh-2002/Orva/backend/internal/builder" "github.com/Harsh-2002/Orva/backend/internal/config" "github.com/Harsh-2002/Orva/backend/internal/database" @@ -38,6 +40,10 @@ type Router struct { firewall *firewall.Manager internalToken string + ai *aipkg.Manager // in-product AI chat assistant (lazily-built LLM gateway) + + keyCache sync.Map // shared API-key cache (keyHash -> *database.APIKey); evicted on key delete + startTime time.Time } @@ -368,6 +374,9 @@ func (r *Router) setupRoutes() { // API Key management routes. keyHandler := &handlers.KeyHandler{ DB: r.db, + // Evict the deleted key from the shared auth cache so revocation is + // immediate (otherwise a cached key keeps authenticating). + InvalidateKey: func(keyHash string) { r.keyCache.Delete(keyHash) }, } r.mux.HandleFunc("POST /api/v1/keys", keyHandler.Create) r.mux.HandleFunc("GET /api/v1/keys", keyHandler.List) @@ -416,7 +425,7 @@ func (r *Router) setupRoutes() { // (or X-Orva-API-Key for parity with REST callers). The handler // owns its own auth gate; /mcp does not start with /api/ so it // naturally bypasses middleware_auth.go. - mcpHandler := orvampc.NewHandler(orvampc.Deps{ + mcpDeps := orvampc.Deps{ DB: r.db, Registry: r.registry, Builder: r.builder, @@ -429,9 +438,35 @@ func (r *Router) setupRoutes() { EventHub: r.eventHub, DataDir: r.cfg.Data.Dir, Version: version.Version, - }) + } + mcpHandler := orvampc.NewHandler(mcpDeps) r.mux.Handle("/mcp", mcpHandler) r.mux.Handle("/mcp/", mcpHandler) + + // In-product AI chat assistant. Same mcpDeps power the in-process tool + // registry the agent dispatches against; provider keys are encrypted with + // the existing secrets cipher. All /api/v1/ai/* paths are gated by the + // standard auth middleware (session cookie or API key). + r.ai = aipkg.New(r.db, r.secrets, mcpDeps) + aiHandler := &AIHandler{Manager: r.ai, DB: r.db} + r.mux.HandleFunc("POST /api/v1/ai/chat", aiHandler.ChatStream) + r.mux.HandleFunc("GET /api/v1/ai/conversations", aiHandler.ListConversations) + r.mux.HandleFunc("POST /api/v1/ai/conversations", aiHandler.CreateConversation) + r.mux.HandleFunc("GET /api/v1/ai/conversations/{id}", aiHandler.GetConversation) + r.mux.HandleFunc("PATCH /api/v1/ai/conversations/{id}", aiHandler.PatchConversation) + r.mux.HandleFunc("DELETE /api/v1/ai/conversations/{id}", aiHandler.DeleteConversation) + r.mux.HandleFunc("GET /api/v1/ai/conversations/{id}/messages", aiHandler.ListMessages) + r.mux.HandleFunc("POST /api/v1/ai/conversations/{id}/regenerate", aiHandler.Regenerate) + r.mux.HandleFunc("POST /api/v1/ai/conversations/{id}/messages/{mid}/edit", aiHandler.EditMessage) + r.mux.HandleFunc("DELETE /api/v1/ai/conversations/{id}/messages/{mid}", aiHandler.DeleteMessage) + r.mux.HandleFunc("POST /api/v1/ai/tool-calls/{id}/approve", aiHandler.ApproveTool) + r.mux.HandleFunc("POST /api/v1/ai/tool-calls/{id}/reject", aiHandler.RejectTool) + r.mux.HandleFunc("GET /api/v1/ai/providers", aiHandler.ListProviders) + r.mux.HandleFunc("POST /api/v1/ai/providers", aiHandler.SaveProvider) + r.mux.HandleFunc("DELETE /api/v1/ai/providers/{id}", aiHandler.DeleteProvider) + r.mux.HandleFunc("GET /api/v1/ai/providers/{id}/models", aiHandler.ListProviderModels) + r.mux.HandleFunc("GET /api/v1/ai/settings", aiHandler.GetSettings) + r.mux.HandleFunc("PUT /api/v1/ai/settings", aiHandler.PutSettings) // RFC 9728 §3.1 — clients MAY look up resource metadata at a path // derived from the protected resource's URL. Serve the same // document at both the bare and the /mcp-suffixed location so MCP @@ -501,7 +536,7 @@ func (r *Router) buildMiddlewareChain() { // Build chain from inside out: Handler -> Logger -> RequestID -> Auth -> BodySize -> CORS chain := loggerMiddleware(r.db, r.eventHub, r.mux) chain = requestIDMiddleware(chain) - chain = authMiddleware(r.db, chain) + chain = authMiddleware(r.db, &r.keyCache, chain) chain = bodySizeMiddleware(maxBody, chain) chain = corsMiddleware(origins, chain) diff --git a/backend/internal/server/server.go b/backend/internal/server/server.go index 8468fe9..2434333 100644 --- a/backend/internal/server/server.go +++ b/backend/internal/server/server.go @@ -530,6 +530,11 @@ func (s *Server) Shutdown(ctx context.Context) error { if s.Scheduler != nil { s.Scheduler.Stop(5 * time.Second) } + // Release the AI LLM gateway pools (HTTP is already drained, so no chat is + // mid-stream). Without this the embedded Bifrost pools leak on every restart. + if s.router != nil && s.router.ai != nil { + s.router.ai.Close() + } return nil } diff --git a/backend/internal/server/ui_dist/assets/AI-BpuQxF1y.css b/backend/internal/server/ui_dist/assets/AI-BpuQxF1y.css new file mode 100644 index 0000000..b8288e9 --- /dev/null +++ b/backend/internal/server/ui_dist/assets/AI-BpuQxF1y.css @@ -0,0 +1 @@ +.cb[data-v-a1ccbe31]{background:var(--color-background);border:1px solid var(--color-border);border-radius:.6rem;overflow:hidden;margin:.5rem 0}.cb-bar[data-v-a1ccbe31]{display:flex;align-items:center;justify-content:space-between;padding:.4rem .7rem;background:var(--color-surface);border-bottom:1px solid var(--color-border)}.cb-lang[data-v-a1ccbe31]{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--color-foreground-muted)}.cb-copy[data-v-a1ccbe31]{display:inline-flex;align-items:center;gap:.35rem;padding:.25rem .55rem;background:var(--color-background);border:1px solid var(--color-border);border-radius:.35rem;color:var(--color-foreground-muted);font-family:var(--font-sans);font-size:11px;cursor:pointer;transition:color .12s,border-color .12s,background .12s}.cb-copy[data-v-a1ccbe31]:hover{color:var(--color-foreground);border-color:color-mix(in srgb,var(--color-primary) 60%,transparent);background:var(--color-surface-hover)}.cb-copy[data-v-a1ccbe31]:focus-visible,.cb-expand[data-v-a1ccbe31]:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px;color:var(--color-foreground)}.cb-scroll[data-v-a1ccbe31]{position:relative}.cb-clamped[data-v-a1ccbe31]{max-height:16rem;overflow:hidden}.cb-pre[data-v-a1ccbe31]{margin:0;padding:.8rem .95rem;overflow-x:auto;font-family:var(--font-mono);font-size:12.5px;line-height:1.6;color:var(--color-foreground-muted);background:var(--color-background)}.cb-pre code[data-v-a1ccbe31]{background:transparent!important;padding:0!important;font-family:inherit;font-size:inherit;line-height:inherit}.cb-fade[data-v-a1ccbe31]{position:absolute;inset:auto 0 0;height:3.5rem;pointer-events:none;background:linear-gradient(180deg,transparent 0%,var(--color-background) 92%)}.cb-expand[data-v-a1ccbe31]{display:flex;align-items:center;gap:.4rem;width:100%;padding:.4rem .7rem;background:var(--color-surface);border:0;border-top:1px solid var(--color-border);color:var(--color-foreground-muted);font-family:var(--font-sans);font-size:11px;cursor:pointer;transition:color .12s,background .12s}.cb-expand[data-v-a1ccbe31]:hover{color:var(--color-foreground);background:var(--color-surface-hover)}.thinking-shimmer[data-v-d5f3c44f]{animation:thinking-pulse-d5f3c44f 1.6s ease-in-out infinite}@keyframes thinking-pulse-d5f3c44f{0%,to{opacity:1}50%{opacity:.55}}@media(prefers-reduced-motion:reduce){.thinking-shimmer[data-v-d5f3c44f]{animation:none}}.md-body[data-v-2f8b301c] p{margin:.25rem 0}.md-body[data-v-2f8b301c] p:first-child{margin-top:0}.md-body[data-v-2f8b301c] p:last-child{margin-bottom:0}.md-body[data-v-2f8b301c] pre{background:var(--color-background, #12111c);border:1px solid var(--color-border, #2d2b42);border-radius:.5rem;padding:.75rem;overflow-x:auto;margin:.5rem 0;font-size:.8125rem}.md-body[data-v-2f8b301c] code{font-family:var(--font-mono, monospace);font-size:.85em}.md-body[data-v-2f8b301c] :not(pre)>code{background:var(--color-surface-hover);padding:.1em .35em;border-radius:.25rem}.md-body[data-v-2f8b301c] a{color:var(--color-link);text-decoration:underline}.md-body[data-v-2f8b301c] ul,.md-body[data-v-2f8b301c] ol{padding-left:1.25rem;margin:.25rem 0}.md-body[data-v-2f8b301c] ul{list-style:disc}.md-body[data-v-2f8b301c] ol{list-style:decimal}.md-body[data-v-2f8b301c] h1,.md-body[data-v-2f8b301c] h2,.md-body[data-v-2f8b301c] h3,.md-body[data-v-2f8b301c] h4,.md-body[data-v-2f8b301c] h5,.md-body[data-v-2f8b301c] h6{font-weight:600;line-height:1.3;margin:.6rem 0 .25rem}.md-body[data-v-2f8b301c] h1{font-size:1.0625rem}.md-body[data-v-2f8b301c] h2{font-size:1rem}.md-body[data-v-2f8b301c] h3{font-size:.9375rem}.md-body[data-v-2f8b301c] h1:first-child,.md-body[data-v-2f8b301c] h2:first-child,.md-body[data-v-2f8b301c] h3:first-child,.md-body[data-v-2f8b301c] h4:first-child{margin-top:0}.md-body[data-v-2f8b301c] strong{font-weight:600}.md-body[data-v-2f8b301c] em{font-style:italic}.md-body[data-v-2f8b301c] hr{border:0;border-top:1px solid var(--color-border, #2d2b42);margin:.75rem 0}.md-body[data-v-2f8b301c] table{border-collapse:collapse;margin:.5rem 0;display:block;overflow-x:auto;font-size:inherit}.md-body[data-v-2f8b301c] th,.md-body[data-v-2f8b301c] td{border:1px solid var(--color-border, #2d2b42);padding:.35rem .6rem;text-align:left}.md-body[data-v-2f8b301c] th{background:var(--color-surface, #1a1929);font-weight:600}.md-body[data-v-2f8b301c] blockquote{border-left:2px solid var(--color-border, #2d2b42);padding-left:.75rem;margin:.5rem 0;color:var(--color-foreground-muted, #a3a3b3)}.dot[data-v-3a0eeb7e]{width:6px;height:6px;border-radius:9999px;background:var(--color-foreground-muted);opacity:.3;animation:typing-dot-3a0eeb7e 1.2s ease-in-out infinite}@keyframes typing-dot-3a0eeb7e{0%,80%,to{opacity:.25}40%{opacity:.85}}@media(prefers-reduced-motion:reduce){.dot[data-v-3a0eeb7e]{animation:none;opacity:.55}} diff --git a/backend/internal/server/ui_dist/assets/AI-K5-FbsA9.js b/backend/internal/server/ui_dist/assets/AI-K5-FbsA9.js new file mode 100644 index 0000000..87ade42 --- /dev/null +++ b/backend/internal/server/ui_dist/assets/AI-K5-FbsA9.js @@ -0,0 +1,25 @@ +import{c as oe,j as k,a as C,b as x,d as D,f as v,t as $,g as N,C as bn,r as R,D as X,E as hn,G as Eu,h as O,P as Lu,k as P,_ as K,F as q,p as Y,s as z,w as Bu,M as mn,H as eu,I as ke,J as st,n as U,K as Le,q as G,o as Se,y as Cu,L as Ce,T as gn,N as ct,Q as lu,R as xn,S as kn,U as _n,e as xu,v as vn,V as yn,W as En,X as Cn}from"./index-CmY58qNN.js";import{D as lt}from"./Drawer-B7DqDJXO.js";import{D as wn}from"./download-DmwbrtdM.js";import{S as An}from"./settings-2-CJPaZu6R.js";import{P as dt}from"./pencil-B6LJWpZU.js";import{T as ku}from"./trash-2-BJzRpJ7Y.js";import{H as ne,j as Dn,p as Fn,a as Sn,b as Tn}from"./github-dark-BrynTfs3.js";import{c as ft}from"./clipboard-CmSw2rR-.js";import{C as Be}from"./check-z8qyH5P-.js";import{C as pt}from"./copy-BzujsGZw.js";import{C as wu}from"./chevron-down-BMYhN6hn.js";import{R as bt}from"./rotate-ccw-DQrtkmfg.js";import{Z as Rn}from"./zap-TwPYrLej.js";import{S as Mn}from"./sparkles-B3_cifRe.js";import{_ as Nn}from"./Modal-zN5wBHcp.js";import{_ as We}from"./Input-B5GdDd-T.js";import{C as In}from"./clock-CbnKorJ0.js";const $n=oe("arrow-down",[["path",{d:"M12 5v14",key:"s699le"}],["path",{d:"m19 12-7 7-7-7",key:"1idqje"}]]);const zn=oe("arrow-up",[["path",{d:"m5 12 7-7 7 7",key:"hav0vg"}],["path",{d:"M12 19V5",key:"x0mq9r"}]]);const Ln=oe("ban",[["path",{d:"M4.929 4.929 19.07 19.071",key:"196cmz"}],["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]]);const ht=oe("brain",[["path",{d:"M12 18V5",key:"adv99a"}],["path",{d:"M15 13a4.17 4.17 0 0 1-3-4 4.17 4.17 0 0 1-3 4",key:"1e3is1"}],["path",{d:"M17.598 6.5A3 3 0 1 0 12 5a3 3 0 1 0-5.598 1.5",key:"1gqd8o"}],["path",{d:"M17.997 5.125a4 4 0 0 1 2.526 5.77",key:"iwvgf7"}],["path",{d:"M18 18a4 4 0 0 0 2-7.464",key:"efp6ie"}],["path",{d:"M19.967 17.483A4 4 0 1 1 12 18a4 4 0 1 1-7.967-.517",key:"1gq6am"}],["path",{d:"M6 18a4 4 0 0 1-2-7.464",key:"k1g0md"}],["path",{d:"M6.003 5.125a4 4 0 0 0-2.526 5.77",key:"q97ue3"}]]);const Bn=oe("chevrons-down-up",[["path",{d:"m7 20 5-5 5 5",key:"13a0gw"}],["path",{d:"m7 4 5 5 5-5",key:"1kwcof"}]]);const On=oe("chevrons-up-down",[["path",{d:"m7 15 5 5 5-5",key:"1hf1tw"}],["path",{d:"m7 9 5-5 5 5",key:"sgt6xg"}]]);const Pn=oe("cpu",[["path",{d:"M12 20v2",key:"1lh1kg"}],["path",{d:"M12 2v2",key:"tus03m"}],["path",{d:"M17 20v2",key:"1rnc9c"}],["path",{d:"M17 2v2",key:"11trls"}],["path",{d:"M2 12h2",key:"1t8f8n"}],["path",{d:"M2 17h2",key:"7oei6x"}],["path",{d:"M2 7h2",key:"asdhe0"}],["path",{d:"M20 12h2",key:"1q8mjw"}],["path",{d:"M20 17h2",key:"1fpfkl"}],["path",{d:"M20 7h2",key:"1o8tra"}],["path",{d:"M7 20v2",key:"4gnj0m"}],["path",{d:"M7 2v2",key:"1i4yhu"}],["rect",{x:"4",y:"4",width:"16",height:"16",rx:"2",key:"1vbyd7"}],["rect",{x:"8",y:"8",width:"8",height:"8",rx:"1",key:"z9xiuo"}]]);const mt=oe("message-square",[["path",{d:"M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z",key:"18887p"}]]);const qn=oe("panel-left",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}],["path",{d:"M9 3v18",key:"fh3hqa"}]]);const Un=oe("square",[["rect",{width:"18",height:"18",x:"3",y:"3",rx:"2",key:"afitv7"}]]);const jn=oe("wrench",[["path",{d:"M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.106-3.105c.32-.322.863-.22.983.218a6 6 0 0 1-8.259 7.057l-7.91 7.91a1 1 0 0 1-2.999-3l7.91-7.91a6 6 0 0 1 7.057-8.259c.438.12.54.662.219.984z",key:"1ngwbx"}]]),Hn={class:"flex h-16 shrink-0 items-center justify-between gap-3 border-b border-border px-4"},Vn={class:"flex min-w-0 items-center gap-2"},Zn={class:"truncate text-sm font-semibold tracking-tight text-white"},Gn={class:"flex items-center gap-0.5"},Wn={__name:"ChatHeader",props:{title:{type:String,default:"Assistant"},canExport:{type:Boolean,default:!1}},emits:["toggle-rail","open-settings","export"],setup(e){return(u,t)=>(k(),C("header",Hn,[x("div",Vn,[x("button",{class:"-ml-1 rounded-md p-2 text-foreground-muted transition-colors hover:bg-surface-hover hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background md:hidden","aria-label":"Conversations",onClick:t[0]||(t[0]=n=>u.$emit("toggle-rail"))},[D(v(qn),{class:"h-4 w-4"})]),D(v(mt),{class:"hidden h-4 w-4 shrink-0 text-foreground-muted md:block"}),x("h1",Zn,$(e.title),1)]),x("div",Gn,[e.canExport?(k(),C("button",{key:0,class:"rounded-md p-2 text-foreground-muted transition-colors hover:bg-surface-hover hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background","aria-label":"Export conversation as Markdown",title:"Export conversation",onClick:t[1]||(t[1]=n=>u.$emit("export"))},[D(v(wn),{class:"h-4 w-4"})])):N("",!0),x("button",{class:"-mr-1 rounded-md p-2 text-foreground-muted transition-colors hover:bg-surface-hover hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background","aria-label":"AI settings",onClick:t[2]||(t[2]=n=>u.$emit("open-settings"))},[D(v(An),{class:"h-4 w-4"})])])]))}},He=bn("ai",()=>{const e=R([]),u=R(null),t=R([]),n=R(!1),r=R(!1),o=R(""),a=R(null),i=R([]),s=R(!1),c=R(null),d=R(""),f=R(""),p=R("off"),b=R([]),l=R(""),m=R(!1);let h=-1,g=null,E=null;function A(){const y={thinking:p.value};return d.value&&(y.provider=d.value),f.value&&(y.model=f.value),y}let T=0;function S(y,_){o.value=y,t.value.push({kind:"error",id:`err-${++T}`,message:y,code:_||""})}function M(y){t.value=t.value.filter(_=>!(_.kind==="error"&&_.id===y))}function L(){t.value=t.value.filter(y=>y.kind!=="error")}async function j(){if(u.value)try{const{data:y}=await X.get(`/ai/conversations/${u.value}`);t.value=$e(y)}catch{}}function Q(){h>=0&&t.value[h]?.kind==="message"||(t.value.push({kind:"message",role:"assistant",parts:[]}),h=t.value.length-1)}function ue(y){const _=t.value[h];if(!_)return;const w={..._,parts:_.parts.slice()};y(w),t.value[h]=w}function ve(y){let _="message",w="";for(const I of y.split(` +`))I.startsWith("event:")?_=I.slice(6).trim():I.startsWith("data:")&&(w+=I.slice(5).trim());let F={};if(w)try{F=JSON.parse(w)}catch{F={raw:w}}return{event:_,data:F}}function Ne(y,_){switch(y){case"conversation":u.value=_.id,e.value.find(w=>w.id===_.id)||e.value.unshift({id:_.id,title:_.title||"New conversation",updated_at:new Date().toISOString()});break;case"message_start":t.value.push({kind:"message",role:"assistant",id:_.message_id,parts:[]}),h=t.value.length-1;break;case"delta":Q(),ue(w=>{const F=w.parts[w.parts.length-1];F&&F.type==="text"?w.parts[w.parts.length-1]={...F,text:F.text+_.text}:w.parts.push({type:"text",text:_.text})});break;case"thinking":Q(),ue(w=>{const F=w.parts.findIndex(I=>I.type==="thinking");F>=0?w.parts[F]={...w.parts[F],text:w.parts[F].text+_.text}:w.parts.unshift({type:"thinking",text:_.text,streaming:!0,startedAt:Date.now()})});break;case"tool_call":t.value.push({kind:"tool",id:_.id,call_id:_.call_id,name:_.name,group:_.group,args:_.args,status:_.requires_approval?"pending_approval":"running",result:null});break;case"tool_result":{for(let w=t.value.length-1;w>=0;w--){const F=t.value[w];if(F.kind==="tool"&&F.id===_.id){t.value[w]={...F,status:_.status,result:_.result};break}}break}case"awaiting_approval":r.value=!0,n.value=!1,h>=0&&ue(w=>{const F=w.parts.findIndex(I=>I.type==="thinking");F>=0&&(w.parts[F]={...w.parts[F],streaming:!1})});break;case"message_end":h>=0&&ue(w=>{const F=w.parts.findIndex(I=>I.type==="thinking");F>=0&&(w.parts[F]={...w.parts[F],streaming:!1})}),h=-1;break;case"done":n.value=!1;break;case"error":S(_.message||"stream error",_.code),n.value=!1;break}}async function he(y,_){const w={"Content-Type":"application/json"},F=hn();F&&(w["X-Orva-API-Key"]=F);const I=await fetch(y,{method:"POST",credentials:"include",headers:w,body:JSON.stringify(_),signal:g?.signal});if(!I.ok||!I.body)throw new Error(`chat request failed (${I.status})`);const Z=I.body.getReader(),se=new TextDecoder;let me="";for(;;){const{done:De,value:dn}=await Z.read();if(De)break;me+=se.decode(dn,{stream:!0});let cu;for(;(cu=me.indexOf(` + +`))>=0;){const zu=me.slice(0,cu);if(me=me.slice(cu+2),zu.trim()){const{event:fn,data:pn}=ve(zu);Ne(fn,pn)}}}}async function ye(y){if(!(!y.trim()||n.value)){o.value="",L(),r.value=!1,E={type:"chat",content:y},t.value.push({kind:"message",role:"user",parts:[{type:"text",text:y}]}),n.value=!0,h=-1,g=new AbortController;try{const _={content:y,...A()};u.value&&(_.conversation_id=u.value),await he("/api/v1/ai/chat",_)}catch(_){_.name!=="AbortError"&&S(_.message)}finally{n.value=!1,g=null,await j()}}}async function te(){if(n.value||!u.value)return;const y=t.value;let _=y.length;for(let w=y.length-1;w>=0&&!(y[w].kind==="message"&&y[w].role==="user");w--)_=w;t.value=y.slice(0,_),o.value="",r.value=!1,E={type:"regenerate"},n.value=!0,h=-1,g=new AbortController;try{await he(`/api/v1/ai/conversations/${u.value}/regenerate`,A())}catch(w){w.name!=="AbortError"&&S(w.message)}finally{n.value=!1,g=null,await j()}}async function we(y,_){if(n.value||!u.value||!_.trim())return;const w=t.value,F=w.findIndex(I=>I.kind==="message"&&I.id===y);if(!(F<0)){t.value=[...w.slice(0,F),{...w[F],parts:[{type:"text",text:_}]}],o.value="",r.value=!1,E={type:"edit",messageId:y,content:_},n.value=!0,h=-1,g=new AbortController;try{await he(`/api/v1/ai/conversations/${u.value}/messages/${y}/edit`,{content:_,...A()})}catch(I){I.name!=="AbortError"&&S(I.message)}finally{n.value=!1,g=null,await j()}}}async function iu(y){if(!u.value||n.value)return;const _=t.value,w=_.findIndex(F=>F.kind==="message"&&F.id===y);if(!(w<0)){t.value=_.slice(0,w);try{await X.delete(`/ai/conversations/${u.value}/messages/${y}`)}catch(F){S(F.message),await j()}}}async function W(){L();const y=E;if(y?.type==="tool")return pe(y.rowId,y.approved);if(y?.type==="regenerate")return te();await j();const _=t.value[t.value.length-1];if(_&&_.kind==="message"&&_.role==="user")return te();if(y?.content)return ye(y.content)}function ae(){g&&g.abort(),n.value=!1}async function pe(y,_){if(n.value)return;o.value="",L(),r.value=!1,E={type:"tool",rowId:y,approved:_},n.value=!0,h=-1,g=new AbortController;const w=_?"approve":"reject";try{await he(`/api/v1/ai/tool-calls/${y}/${w}`,{})}catch(F){F.name!=="AbortError"&&S(F.message)}finally{n.value=!1,g=null,await j()}}const Ae=y=>pe(y,!0),Ie=y=>pe(y,!1);function $e(y){const _=[],w={};for(const F of y.tool_calls||[])(w[F.message_id]||=[]).push(F);for(const F of y.messages||[]){const I=Xt(F.parts);if(F.role==="user")_.push({kind:"message",role:"user",id:F.id,parts:I});else if(F.role==="assistant"){_.push({kind:"message",role:"assistant",id:F.id,parts:I});for(const Z of w[F.id]||[])_.push({kind:"tool",id:Z.id,call_id:Z.call_id,name:Z.tool_name,group:Z.tool_group,args:Z.args,status:Z.status,result:Z.result})}}return _}function Xt(y){let _=[];try{_=JSON.parse(y||"[]")}catch{_=[]}return _.filter(w=>w.type==="text"||w.type==="thinking")}async function Yt(){const{data:y}=await X.get("/ai/conversations");e.value=y.conversations||[]}async function en(y){ae(),h=-1;const{data:_}=await X.get(`/ai/conversations/${y}`);u.value=y,t.value=$e(_),r.value=!1,o.value=""}function Iu(){ae(),h=-1,u.value=null,t.value=[],r.value=!1,o.value=""}async function un(y){await X.delete(`/ai/conversations/${y}`),e.value=e.value.filter(_=>_.id!==y),u.value===y&&Iu()}function tn(){const y=t.value;if(!y.length)return;const _=e.value.find(se=>se.id===u.value),w=[`# ${_?.title||"Conversation"}`,""];for(const se of y)if(se.kind==="message"){const me=(se.parts||[]).filter(De=>De.type==="text"&&De.text).map(De=>De.text).join(` + +`).trim();me&&w.push(`## ${se.role==="user"?"You":"Assistant"}`,"",me,"")}else se.kind==="tool"&&w.push(`> tool \`${se.name}\` — ${se.status}`,"");const F=new Blob([w.join(` +`)],{type:"text/markdown"}),I=URL.createObjectURL(F),Z=document.createElement("a");Z.href=I,Z.download=`${(_?.title||"conversation").replace(/[^\w.-]+/g,"-").slice(0,60)||"conversation"}.md`,document.body.appendChild(Z),Z.click(),Z.remove(),URL.revokeObjectURL(I)}async function nn(y,_){const w=(_||"").trim();if(!w)return;const{data:F}=await X.patch(`/ai/conversations/${y}`,{title:w}),I=e.value.find(Z=>Z.id===y);I&&(I.title=F.conversation?.title??w)}async function rn(){const{data:y}=await X.get("/ai/settings");a.value=y.settings,(!p.value||p.value==="off")&&(p.value=y.settings?.thinking_level||"off")}async function on(y){const{data:_}=await X.put("/ai/settings",y);a.value=_.settings}async function au(){try{const{data:y}=await X.get("/ai/providers");if(i.value=y.providers||[],c.value){if(!i.value.find(_=>_.id===c.value)){const _=i.value[0];_?await su(_.id):(c.value=null,d.value="",f.value="",b.value=[])}}else{const _=i.value.find(w=>w.enabled)||i.value[0];_&&await su(_.id)}}finally{s.value=!0}}async function su(y){const _=i.value.find(w=>w.id===y);if(_&&(c.value=_.id,d.value=_.provider,f.value="",await $u(_.id),b.value.length)){const w=b.value.find(F=>/\b(mini|small|fast|flash|lite|nano|haiku)\b/i.test(F.id));f.value=(w||b.value[0]).id}}async function $u(y){m.value=!0,l.value="",b.value=[];try{const{data:_}=await X.get(`/ai/providers/${y}/models`);b.value=_.models||[],_.error&&(l.value=_.error)}catch(_){l.value=_.message}finally{m.value=!1}}function an(y){f.value=y}function sn(y){p.value=y}async function cn(y){await X.post("/ai/providers",y),await au()}async function ln(y){await X.delete(`/ai/providers/${y}`),await au()}return{conversations:e,activeId:u,timeline:t,streaming:n,awaitingApproval:r,error:o,settings:a,providers:i,providersLoaded:s,selectedProviderId:c,selectedProvider:d,selectedModel:f,thinking:p,models:b,modelsError:l,modelsLoading:m,sendMessage:ye,approveTool:Ae,rejectTool:Ie,stop:ae,regenerate:te,editAndResend:we,deleteMessageFrom:iu,retry:W,dismissError:M,loadConversations:Yt,openConversation:en,newConversation:Iu,deleteConversation:un,renameConversation:nn,exportActive:tn,loadSettings:rn,saveSettings:on,loadProviders:au,saveProvider:cn,deleteProvider:ln,selectProvider:su,selectModel:an,setThinking:sn,loadProviderModels:$u}}),Kn={class:"flex h-full flex-col"},Jn={key:0,class:"flex h-16 shrink-0 items-center justify-between px-4 border-b border-border"},Qn={class:"flex-1 overflow-y-auto scrollable p-2 space-y-0.5"},Xn=["aria-current","onClick"],Yn={class:"flex-1 truncate"},er=["onClick"],ur=["onClick"],tr={key:1,class:"px-2.5 py-2 text-xs text-foreground-muted"},Ou={__name:"ConversationRail",props:{embedded:{type:Boolean,default:!1}},emits:["select"],setup(e,{emit:u}){const t=He(),n=Eu();async function r(c){const d=await n.prompt({title:"Rename conversation",defaultValue:c.title||"",placeholder:"Conversation name",confirmLabel:"Rename"});d!=null&&d.trim()&&t.renameConversation(c.id,d.trim())}async function o(c){await n.ask({title:"Delete conversation?",message:"This permanently deletes the conversation and all its messages.",danger:!0,confirmLabel:"Delete"})&&t.deleteConversation(c)}const a=u;function i(){t.newConversation(),a("select")}function s(c){t.openConversation(c),a("select")}return(c,d)=>(k(),C("div",Kn,[e.embedded?N("",!0):(k(),C("div",Jn,[d[1]||(d[1]=x("span",{class:"text-sm font-semibold tracking-tight text-white"},"Conversations",-1)),D(K,{size:"xs",variant:"secondary",onClick:i},{default:O(()=>[D(v(Lu),{class:"h-3.5 w-3.5"}),d[0]||(d[0]=P(" New ",-1))]),_:1})])),x("div",Qn,[e.embedded?(k(),C("button",{key:0,class:"touch-expand-sm mb-1 flex w-full items-center gap-2 rounded-md border border-border px-2.5 py-2 text-left text-sm text-foreground transition-colors hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary",onClick:i},[D(v(Lu),{class:"h-3.5 w-3.5 shrink-0"}),d[2]||(d[2]=P(" New conversation ",-1))])):N("",!0),(k(!0),C(q,null,Y(v(t).conversations,f=>(k(),C("div",{key:f.id,class:z(["group flex w-full items-center gap-0.5 rounded-md pr-1 transition-colors",f.id===v(t).activeId?"bg-primary/15":"hover:bg-surface-hover"])},[x("button",{class:z(["touch-expand-sm flex min-w-0 flex-1 items-center gap-2 rounded-md px-2.5 py-2 text-left text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary",f.id===v(t).activeId?"text-white":"text-foreground-muted group-hover:text-white"]),"aria-current":f.id===v(t).activeId?"page":void 0,onClick:p=>s(f.id)},[D(v(mt),{class:"h-3.5 w-3.5 shrink-0 opacity-70"}),x("span",Yn,$(f.title||"New conversation"),1)],10,Xn),x("button",{type:"button",class:"touch-expand-xs shrink-0 rounded-md p-2 text-foreground-muted opacity-0 transition-opacity hover:bg-surface-hover hover:text-white focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary group-hover:opacity-100 max-md:opacity-100",title:"Rename conversation","aria-label":"Rename conversation",onClick:Bu(p=>r(f),["stop"])},[D(v(dt),{class:"h-3.5 w-3.5"})],8,er),x("button",{type:"button",class:"touch-expand-xs shrink-0 rounded-md p-2 text-foreground-muted opacity-0 transition-opacity hover:bg-surface-hover hover:text-danger-fg focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary group-hover:opacity-100 max-md:opacity-100",title:"Delete conversation","aria-label":"Delete conversation",onClick:Bu(p=>o(f.id),["stop"])},[D(v(ku),{class:"h-3.5 w-3.5"})],8,ur)],2))),128)),v(t).conversations.length?N("",!0):(k(),C("p",tr," No conversations yet. "))])]))}},nr={class:"text-center"},rr={class:"mx-auto mb-4 flex h-11 w-11 items-center justify-center rounded-xl bg-primary/15 text-primary"},or={class:"mx-auto mt-6 grid max-w-xl gap-2 sm:grid-cols-2"},ir=["onClick"],ar={class:"line-clamp-2"},sr={__name:"EmptyState",emits:["pick"],setup(e){const u=["How many functions do I have?","Which function ran most recently?","Any errors in the last 24 hours?","Show my most recent executions.","List my deployed functions.","Summarize today’s invocation errors.","Show failed deployments and why.","What’s my system health right now?","Show storage usage for my instance.","List my cron schedules.","Are any background jobs failing?","Check for failed webhook deliveries.","Which runtimes are available?","Show my slowest functions by duration.","Which functions have egress enabled?","List my secrets by name only.","Write a Python function that returns the current UTC time.","Write a Node function that echoes the request body.","Create an hourly cron schedule for a function.","Walk me through deploying a new function."];function t(r,o){const a=[...r];for(let i=a.length-1;i>0;i--){const s=Math.floor(Math.random()*(i+1));[a[i],a[s]]=[a[s],a[i]]}return a.slice(0,o)}const n=R(t(u,4));return(r,o)=>(k(),C("div",nr,[x("div",rr,[D(v(mn),{class:"h-5 w-5"})]),o[0]||(o[0]=x("h2",{class:"text-lg font-semibold tracking-tight text-white"}," What would you like to do? ",-1)),o[1]||(o[1]=x("p",{class:"mx-auto mt-1.5 max-w-md text-sm leading-relaxed text-foreground-muted"}," Ask about your functions, logs, deployments, and operations. Or have me create, deploy, and invoke functions for you. ",-1)),x("div",or,[(k(!0),C(q,null,Y(n.value,(a,i)=>(k(),C("button",{key:i,type:"button",class:"flex min-h-[4.25rem] items-center rounded-lg border border-border bg-surface/50 px-3.5 py-3 text-left text-[13px] leading-snug text-foreground-muted transition-colors hover:border-foreground-muted/40 hover:bg-surface-hover hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background",onClick:s=>r.$emit("pick",a)},[x("span",ar,$(a),1)],8,ir))),128))])]))}},Pu={};function cr(e){let u=Pu[e];if(u)return u;u=Pu[e]=[];for(let t=0;t<128;t++){const n=String.fromCharCode(t);u.push(n)}for(let t=0;t=55296&&d<=57343?r+="���":r+=String.fromCharCode(d),o+=6;continue}}if((i&248)===240&&o+91114111?r+="����":(f-=65536,r+=String.fromCharCode(55296+(f>>10),56320+(f&1023))),o+=9;continue}}r+="�"}return r})}Te.defaultChars=";/?:@&=+$,#";Te.componentChars="";const qu={};function lr(e){let u=qu[e];if(u)return u;u=qu[e]=[];for(let t=0;t<128;t++){const n=String.fromCharCode(t);/^[0-9a-z]$/i.test(n)?u.push(n):u.push("%"+("0"+t.toString(16).toUpperCase()).slice(-2))}for(let t=0;t"u"&&(t=!0);const n=lr(u);let r="";for(let o=0,a=e.length;o=55296&&i<=57343){if(i>=55296&&i<=56319&&o+1=56320&&s<=57343){r+=encodeURIComponent(e[o]+e[o+1]),o++;continue}}r+="%EF%BF%BD";continue}r+=encodeURIComponent(e[o])}return r}Ve.defaultChars=";/?:@&=+$,-_.!~*'()#";Ve.componentChars="-_.!~*'()";function Au(e){let u="";return u+=e.protocol||"",u+=e.slashes?"//":"",u+=e.auth?e.auth+"@":"",e.hostname&&e.hostname.indexOf(":")!==-1?u+="["+e.hostname+"]":u+=e.hostname||"",u+=e.port?":"+e.port:"",u+=e.pathname||"",u+=e.search||"",u+=e.hash||"",u}function Qe(){this.protocol=null,this.slashes=null,this.auth=null,this.port=null,this.hostname=null,this.hash=null,this.search=null,this.pathname=null}const dr=/^([a-z0-9.+-]+:)/i,fr=/:[0-9]*$/,pr=/^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,br=["<",">",'"',"`"," ","\r",` +`," "],hr=["{","}","|","\\","^","`"].concat(br),mr=["'"].concat(hr),Uu=["%","/","?",";","#"].concat(mr),ju=["/","?","#"],gr=255,Hu=/^[+a-z0-9A-Z_-]{0,63}$/,xr=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,Vu={javascript:!0,"javascript:":!0},Zu={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0};function Du(e,u){if(e&&e instanceof Qe)return e;const t=new Qe;return t.parse(e,u),t}Qe.prototype.parse=function(e,u){let t,n,r,o=e;if(o=o.trim(),!u&&e.split("#").length===1){const c=pr.exec(o);if(c)return this.pathname=c[1],c[2]&&(this.search=c[2]),this}let a=dr.exec(o);if(a&&(a=a[0],t=a.toLowerCase(),this.protocol=a,o=o.substr(a.length)),(u||a||o.match(/^\/\/[^@\/]+@[^@\/]+/))&&(r=o.substr(0,2)==="//",r&&!(a&&Vu[a])&&(o=o.substr(2),this.slashes=!0)),!Vu[a]&&(r||a&&!Zu[a])){let c=-1;for(let l=0;l127?E+="x":E+=g[A];if(!E.match(Hu)){const A=l.slice(0,m),T=l.slice(m+1),S=g.match(xr);S&&(A.push(S[1]),T.unshift(S[2])),T.length&&(o=T.join(".")+o),this.hostname=A.join(".");break}}}}this.hostname.length>gr&&(this.hostname=""),b&&(this.hostname=this.hostname.substr(1,this.hostname.length-2))}const i=o.indexOf("#");i!==-1&&(this.hash=o.substr(i),o=o.slice(0,i));const s=o.indexOf("?");return s!==-1&&(this.search=o.substr(s),o=o.slice(0,s)),o&&(this.pathname=o),Zu[t]&&this.hostname&&!this.pathname&&(this.pathname=""),this};Qe.prototype.parseHost=function(e){let u=fr.exec(e);u&&(u=u[0],u!==":"&&(this.port=u.substr(1)),e=e.substr(0,e.length-u.length)),e&&(this.hostname=e)};const kr=Object.freeze(Object.defineProperty({__proto__:null,decode:Te,encode:Ve,format:Au,parse:Du},Symbol.toStringTag,{value:"Module"})),gt=/[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/,xt=/[\0-\x1F\x7F-\x9F]/,_r=/[\xAD\u0600-\u0605\u061C\u06DD\u070F\u0890\u0891\u08E2\u180E\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u206F\uFEFF\uFFF9-\uFFFB]|\uD804[\uDCBD\uDCCD]|\uD80D[\uDC30-\uDC3F]|\uD82F[\uDCA0-\uDCA3]|\uD834[\uDD73-\uDD7A]|\uDB40[\uDC01\uDC20-\uDC7F]/,Fu=/[!-#%-\*,-\/:;\?@\[-\]_\{\}\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061D-\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u09FD\u0A76\u0AF0\u0C77\u0C84\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1B7D\u1B7E\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E4F\u2E52-\u2E5D\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]|\uD800[\uDD00-\uDD02\uDF9F\uDFD0]|\uD801\uDD6F|\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD803[\uDEAD\uDF55-\uDF59\uDF86-\uDF89]|\uD804[\uDC47-\uDC4D\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC8\uDDCD\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD805[\uDC4B-\uDC4F\uDC5A\uDC5B\uDC5D\uDCC6\uDDC1-\uDDD7\uDE41-\uDE43\uDE60-\uDE6C\uDEB9\uDF3C-\uDF3E]|\uD806[\uDC3B\uDD44-\uDD46\uDDE2\uDE3F-\uDE46\uDE9A-\uDE9C\uDE9E-\uDEA2\uDF00-\uDF09]|\uD807[\uDC41-\uDC45\uDC70\uDC71\uDEF7\uDEF8\uDF43-\uDF4F\uDFFF]|\uD809[\uDC70-\uDC74]|\uD80B[\uDFF1\uDFF2]|\uD81A[\uDE6E\uDE6F\uDEF5\uDF37-\uDF3B\uDF44]|\uD81B[\uDE97-\uDE9A\uDFE2]|\uD82F\uDC9F|\uD836[\uDE87-\uDE8B]|\uD83A[\uDD5E\uDD5F]/,kt=/[\$\+<->\^`\|~\xA2-\xA6\xA8\xA9\xAC\xAE-\xB1\xB4\xB8\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u02FF\u0375\u0384\u0385\u03F6\u0482\u058D-\u058F\u0606-\u0608\u060B\u060E\u060F\u06DE\u06E9\u06FD\u06FE\u07F6\u07FE\u07FF\u0888\u09F2\u09F3\u09FA\u09FB\u0AF1\u0B70\u0BF3-\u0BFA\u0C7F\u0D4F\u0D79\u0E3F\u0F01-\u0F03\u0F13\u0F15-\u0F17\u0F1A-\u0F1F\u0F34\u0F36\u0F38\u0FBE-\u0FC5\u0FC7-\u0FCC\u0FCE\u0FCF\u0FD5-\u0FD8\u109E\u109F\u1390-\u1399\u166D\u17DB\u1940\u19DE-\u19FF\u1B61-\u1B6A\u1B74-\u1B7C\u1FBD\u1FBF-\u1FC1\u1FCD-\u1FCF\u1FDD-\u1FDF\u1FED-\u1FEF\u1FFD\u1FFE\u2044\u2052\u207A-\u207C\u208A-\u208C\u20A0-\u20C0\u2100\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F\u218A\u218B\u2190-\u2307\u230C-\u2328\u232B-\u2426\u2440-\u244A\u249C-\u24E9\u2500-\u2767\u2794-\u27C4\u27C7-\u27E5\u27F0-\u2982\u2999-\u29D7\u29DC-\u29FB\u29FE-\u2B73\u2B76-\u2B95\u2B97-\u2BFF\u2CE5-\u2CEA\u2E50\u2E51\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFF\u3004\u3012\u3013\u3020\u3036\u3037\u303E\u303F\u309B\u309C\u3190\u3191\u3196-\u319F\u31C0-\u31E3\u31EF\u3200-\u321E\u322A-\u3247\u3250\u3260-\u327F\u328A-\u32B0\u32C0-\u33FF\u4DC0-\u4DFF\uA490-\uA4C6\uA700-\uA716\uA720\uA721\uA789\uA78A\uA828-\uA82B\uA836-\uA839\uAA77-\uAA79\uAB5B\uAB6A\uAB6B\uFB29\uFBB2-\uFBC2\uFD40-\uFD4F\uFDCF\uFDFC-\uFDFF\uFE62\uFE64-\uFE66\uFE69\uFF04\uFF0B\uFF1C-\uFF1E\uFF3E\uFF40\uFF5C\uFF5E\uFFE0-\uFFE6\uFFE8-\uFFEE\uFFFC\uFFFD]|\uD800[\uDD37-\uDD3F\uDD79-\uDD89\uDD8C-\uDD8E\uDD90-\uDD9C\uDDA0\uDDD0-\uDDFC]|\uD802[\uDC77\uDC78\uDEC8]|\uD805\uDF3F|\uD807[\uDFD5-\uDFF1]|\uD81A[\uDF3C-\uDF3F\uDF45]|\uD82F\uDC9C|\uD833[\uDF50-\uDFC3]|\uD834[\uDC00-\uDCF5\uDD00-\uDD26\uDD29-\uDD64\uDD6A-\uDD6C\uDD83\uDD84\uDD8C-\uDDA9\uDDAE-\uDDEA\uDE00-\uDE41\uDE45\uDF00-\uDF56]|\uD835[\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3]|\uD836[\uDC00-\uDDFF\uDE37-\uDE3A\uDE6D-\uDE74\uDE76-\uDE83\uDE85\uDE86]|\uD838[\uDD4F\uDEFF]|\uD83B[\uDCAC\uDCB0\uDD2E\uDEF0\uDEF1]|\uD83C[\uDC00-\uDC2B\uDC30-\uDC93\uDCA0-\uDCAE\uDCB1-\uDCBF\uDCC1-\uDCCF\uDCD1-\uDCF5\uDD0D-\uDDAD\uDDE6-\uDE02\uDE10-\uDE3B\uDE40-\uDE48\uDE50\uDE51\uDE60-\uDE65\uDF00-\uDFFF]|\uD83D[\uDC00-\uDED7\uDEDC-\uDEEC\uDEF0-\uDEFC\uDF00-\uDF76\uDF7B-\uDFD9\uDFE0-\uDFEB\uDFF0]|\uD83E[\uDC00-\uDC0B\uDC10-\uDC47\uDC50-\uDC59\uDC60-\uDC87\uDC90-\uDCAD\uDCB0\uDCB1\uDD00-\uDE53\uDE60-\uDE6D\uDE70-\uDE7C\uDE80-\uDE88\uDE90-\uDEBD\uDEBF-\uDEC5\uDECE-\uDEDB\uDEE0-\uDEE8\uDEF0-\uDEF8\uDF00-\uDF92\uDF94-\uDFCA]/,_t=/[ \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]/,vr=Object.freeze(Object.defineProperty({__proto__:null,Any:gt,Cc:xt,Cf:_r,P:Fu,S:kt,Z:_t},Symbol.toStringTag,{value:"Module"})),yr=new Uint16Array('ᵁ<Õıʊҝջאٵ۞ޢߖࠏ੊ઑඡ๭༉༦჊ረዡᐕᒝᓃᓟᔥ\0\0\0\0\0\0ᕫᛍᦍᰒᷝ὾⁠↰⊍⏀⏻⑂⠤⤒ⴈ⹈⿎〖㊺㘹㞬㣾㨨㩱㫠㬮ࠀEMabcfglmnoprstu\\bfms„‹•˜¦³¹ÈÏlig耻Æ䃆P耻&䀦cute耻Á䃁reve;䄂Āiyx}rc耻Â䃂;䐐r;쀀𝔄rave耻À䃀pha;䎑acr;䄀d;橓Āgp¡on;䄄f;쀀𝔸plyFunction;恡ing耻Å䃅Ācs¾Ãr;쀀𝒜ign;扔ilde耻Ã䃃ml耻Ä䃄ЀaceforsuåûþėĜĢħĪĀcrêòkslash;或Ŷöø;櫧ed;挆y;䐑ƀcrtąċĔause;戵noullis;愬a;䎒r;쀀𝔅pf;쀀𝔹eve;䋘còēmpeq;扎܀HOacdefhilorsuōőŖƀƞƢƵƷƺǜȕɳɸɾcy;䐧PY耻©䂩ƀcpyŝŢźute;䄆Ā;iŧŨ拒talDifferentialD;慅leys;愭ȀaeioƉƎƔƘron;䄌dil耻Ç䃇rc;䄈nint;戰ot;䄊ĀdnƧƭilla;䂸terDot;䂷òſi;䎧rcleȀDMPTLJNjǑǖot;抙inus;抖lus;投imes;抗oĀcsǢǸkwiseContourIntegral;戲eCurlyĀDQȃȏoubleQuote;思uote;怙ȀlnpuȞȨɇɕonĀ;eȥȦ户;橴ƀgitȯȶȺruent;扡nt;戯ourIntegral;戮ĀfrɌɎ;愂oduct;成nterClockwiseContourIntegral;戳oss;樯cr;쀀𝒞pĀ;Cʄʅ拓ap;才րDJSZacefiosʠʬʰʴʸˋ˗ˡ˦̳ҍĀ;oŹʥtrahd;椑cy;䐂cy;䐅cy;䐏ƀgrsʿ˄ˇger;怡r;憡hv;櫤Āayː˕ron;䄎;䐔lĀ;t˝˞戇a;䎔r;쀀𝔇Āaf˫̧Ācm˰̢riticalȀADGT̖̜̀̆cute;䂴oŴ̋̍;䋙bleAcute;䋝rave;䁠ilde;䋜ond;拄ferentialD;慆Ѱ̽\0\0\0͔͂\0Ѕf;쀀𝔻ƀ;DE͈͉͍䂨ot;惜qual;扐blèCDLRUVͣͲ΂ϏϢϸontourIntegraìȹoɴ͹\0\0ͻ»͉nArrow;懓Āeo·ΤftƀARTΐΖΡrrow;懐ightArrow;懔eåˊngĀLRΫτeftĀARγιrrow;柸ightArrow;柺ightArrow;柹ightĀATϘϞrrow;懒ee;抨pɁϩ\0\0ϯrrow;懑ownArrow;懕erticalBar;戥ǹABLRTaВЪаўѿͼrrowƀ;BUНОТ憓ar;椓pArrow;懵reve;䌑eft˒к\0ц\0ѐightVector;楐eeVector;楞ectorĀ;Bљњ憽ar;楖ightǔѧ\0ѱeeVector;楟ectorĀ;BѺѻ懁ar;楗eeĀ;A҆҇护rrow;憧ĀctҒҗr;쀀𝒟rok;䄐ࠀNTacdfglmopqstuxҽӀӄӋӞӢӧӮӵԡԯԶՒ՝ՠեG;䅊H耻Ð䃐cute耻É䃉ƀaiyӒӗӜron;䄚rc耻Ê䃊;䐭ot;䄖r;쀀𝔈rave耻È䃈ement;戈ĀapӺӾcr;䄒tyɓԆ\0\0ԒmallSquare;旻erySmallSquare;斫ĀgpԦԪon;䄘f;쀀𝔼silon;䎕uĀaiԼՉlĀ;TՂՃ橵ilde;扂librium;懌Āci՗՚r;愰m;橳a;䎗ml耻Ë䃋Āipժկsts;戃onentialE;慇ʀcfiosօֈ֍ֲ׌y;䐤r;쀀𝔉lledɓ֗\0\0֣mallSquare;旼erySmallSquare;斪Ͱֺ\0ֿ\0\0ׄf;쀀𝔽All;戀riertrf;愱cò׋؀JTabcdfgorstר׬ׯ׺؀ؒؖ؛؝أ٬ٲcy;䐃耻>䀾mmaĀ;d׷׸䎓;䏜reve;䄞ƀeiy؇،ؐdil;䄢rc;䄜;䐓ot;䄠r;쀀𝔊;拙pf;쀀𝔾eater̀EFGLSTصلَٖٛ٦qualĀ;Lؾؿ扥ess;招ullEqual;执reater;檢ess;扷lantEqual;橾ilde;扳cr;쀀𝒢;扫ЀAacfiosuڅڋږڛڞڪھۊRDcy;䐪Āctڐڔek;䋇;䁞irc;䄤r;愌lbertSpace;愋ǰگ\0ڲf;愍izontalLine;攀Āctۃۅòکrok;䄦mpńېۘownHumðįqual;扏܀EJOacdfgmnostuۺ۾܃܇܎ܚܞܡܨ݄ݸދޏޕcy;䐕lig;䄲cy;䐁cute耻Í䃍Āiyܓܘrc耻Î䃎;䐘ot;䄰r;愑rave耻Ì䃌ƀ;apܠܯܿĀcgܴܷr;䄪inaryI;慈lieóϝǴ݉\0ݢĀ;eݍݎ戬Āgrݓݘral;戫section;拂isibleĀCTݬݲomma;恣imes;恢ƀgptݿރވon;䄮f;쀀𝕀a;䎙cr;愐ilde;䄨ǫޚ\0ޞcy;䐆l耻Ï䃏ʀcfosuެ޷޼߂ߐĀiyޱ޵rc;䄴;䐙r;쀀𝔍pf;쀀𝕁ǣ߇\0ߌr;쀀𝒥rcy;䐈kcy;䐄΀HJacfosߤߨ߽߬߱ࠂࠈcy;䐥cy;䐌ppa;䎚Āey߶߻dil;䄶;䐚r;쀀𝔎pf;쀀𝕂cr;쀀𝒦րJTaceflmostࠥࠩࠬࡐࡣ঳সে্਷ੇcy;䐉耻<䀼ʀcmnpr࠷࠼ࡁࡄࡍute;䄹bda;䎛g;柪lacetrf;愒r;憞ƀaeyࡗ࡜ࡡron;䄽dil;䄻;䐛Āfsࡨ॰tԀACDFRTUVarࡾࢩࢱࣦ࣠ࣼयज़ΐ४Ānrࢃ࢏gleBracket;柨rowƀ;BR࢙࢚࢞憐ar;懤ightArrow;懆eiling;挈oǵࢷ\0ࣃbleBracket;柦nǔࣈ\0࣒eeVector;楡ectorĀ;Bࣛࣜ懃ar;楙loor;挊ightĀAV࣯ࣵrrow;憔ector;楎Āerँगeƀ;AVउऊऐ抣rrow;憤ector;楚iangleƀ;BEतथऩ抲ar;槏qual;抴pƀDTVषूौownVector;楑eeVector;楠ectorĀ;Bॖॗ憿ar;楘ectorĀ;B॥०憼ar;楒ightáΜs̀EFGLSTॾঋকঝঢভqualGreater;拚ullEqual;扦reater;扶ess;檡lantEqual;橽ilde;扲r;쀀𝔏Ā;eঽা拘ftarrow;懚idot;䄿ƀnpw৔ਖਛgȀLRlr৞৷ਂਐeftĀAR০৬rrow;柵ightArrow;柷ightArrow;柶eftĀarγਊightáοightáϊf;쀀𝕃erĀLRਢਬeftArrow;憙ightArrow;憘ƀchtਾੀੂòࡌ;憰rok;䅁;扪Ѐacefiosuਗ਼੝੠੷੼અઋ઎p;椅y;䐜Ādl੥੯iumSpace;恟lintrf;愳r;쀀𝔐nusPlus;戓pf;쀀𝕄cò੶;䎜ҀJacefostuણધભીଔଙඑ඗ඞcy;䐊cute;䅃ƀaey઴હાron;䅇dil;䅅;䐝ƀgswે૰଎ativeƀMTV૓૟૨ediumSpace;怋hiĀcn૦૘ë૙eryThiî૙tedĀGL૸ଆreaterGreateòٳessLesóੈLine;䀊r;쀀𝔑ȀBnptଢନଷ଺reak;恠BreakingSpace;䂠f;愕ڀ;CDEGHLNPRSTV୕ୖ୪୼஡௫ఄ౞಄ದ೘ൡඅ櫬Āou୛୤ngruent;扢pCap;扭oubleVerticalBar;戦ƀlqxஃஊ஛ement;戉ualĀ;Tஒஓ扠ilde;쀀≂̸ists;戄reater΀;EFGLSTஶஷ஽௉௓௘௥扯qual;扱ullEqual;쀀≧̸reater;쀀≫̸ess;批lantEqual;쀀⩾̸ilde;扵umpń௲௽ownHump;쀀≎̸qual;쀀≏̸eĀfsఊధtTriangleƀ;BEచఛడ拪ar;쀀⧏̸qual;括s̀;EGLSTవశ఼ౄోౘ扮qual;扰reater;扸ess;쀀≪̸lantEqual;쀀⩽̸ilde;扴estedĀGL౨౹reaterGreater;쀀⪢̸essLess;쀀⪡̸recedesƀ;ESಒಓಛ技qual;쀀⪯̸lantEqual;拠ĀeiಫಹverseElement;戌ghtTriangleƀ;BEೋೌ೒拫ar;쀀⧐̸qual;拭ĀquೝഌuareSuĀbp೨೹setĀ;E೰ೳ쀀⊏̸qual;拢ersetĀ;Eഃആ쀀⊐̸qual;拣ƀbcpഓതൎsetĀ;Eഛഞ쀀⊂⃒qual;抈ceedsȀ;ESTലള഻െ抁qual;쀀⪰̸lantEqual;拡ilde;쀀≿̸ersetĀ;E൘൛쀀⊃⃒qual;抉ildeȀ;EFT൮൯൵ൿ扁qual;扄ullEqual;扇ilde;扉erticalBar;戤cr;쀀𝒩ilde耻Ñ䃑;䎝܀Eacdfgmoprstuvලෂ෉෕ෛ෠෧෼ขภยา฿ไlig;䅒cute耻Ó䃓Āiy෎ීrc耻Ô䃔;䐞blac;䅐r;쀀𝔒rave耻Ò䃒ƀaei෮ෲ෶cr;䅌ga;䎩cron;䎟pf;쀀𝕆enCurlyĀDQฎบoubleQuote;怜uote;怘;橔Āclวฬr;쀀𝒪ash耻Ø䃘iŬื฼de耻Õ䃕es;樷ml耻Ö䃖erĀBP๋๠Āar๐๓r;怾acĀek๚๜;揞et;掴arenthesis;揜Ҁacfhilors๿ງຊຏຒດຝະ໼rtialD;戂y;䐟r;쀀𝔓i;䎦;䎠usMinus;䂱Āipຢອncareplanåڝf;愙Ȁ;eio຺ູ໠໤檻cedesȀ;EST່້໏໚扺qual;檯lantEqual;扼ilde;找me;怳Ādp໩໮uct;戏ortionĀ;aȥ໹l;戝Āci༁༆r;쀀𝒫;䎨ȀUfos༑༖༛༟OT耻"䀢r;쀀𝔔pf;愚cr;쀀𝒬؀BEacefhiorsu༾གྷཇའཱིྦྷྪྭ႖ႩႴႾarr;椐G耻®䂮ƀcnrཎནབute;䅔g;柫rĀ;tཛྷཝ憠l;椖ƀaeyཧཬཱron;䅘dil;䅖;䐠Ā;vླྀཹ愜erseĀEUྂྙĀlq྇ྎement;戋uilibrium;懋pEquilibrium;楯r»ཹo;䎡ghtЀACDFTUVa࿁࿫࿳ဢဨၛႇϘĀnr࿆࿒gleBracket;柩rowƀ;BL࿜࿝࿡憒ar;懥eftArrow;懄eiling;按oǵ࿹\0စbleBracket;柧nǔည\0နeeVector;楝ectorĀ;Bဝသ懂ar;楕loor;挋Āerိ၃eƀ;AVဵံြ抢rrow;憦ector;楛iangleƀ;BEၐၑၕ抳ar;槐qual;抵pƀDTVၣၮၸownVector;楏eeVector;楜ectorĀ;Bႂႃ憾ar;楔ectorĀ;B႑႒懀ar;楓Āpuႛ႞f;愝ndImplies;楰ightarrow;懛ĀchႹႼr;愛;憱leDelayed;槴ڀHOacfhimoqstuფჱჷჽᄙᄞᅑᅖᅡᅧᆵᆻᆿĀCcჩხHcy;䐩y;䐨FTcy;䐬cute;䅚ʀ;aeiyᄈᄉᄎᄓᄗ檼ron;䅠dil;䅞rc;䅜;䐡r;쀀𝔖ortȀDLRUᄪᄴᄾᅉownArrow»ОeftArrow»࢚ightArrow»࿝pArrow;憑gma;䎣allCircle;战pf;쀀𝕊ɲᅭ\0\0ᅰt;戚areȀ;ISUᅻᅼᆉᆯ斡ntersection;抓uĀbpᆏᆞsetĀ;Eᆗᆘ抏qual;抑ersetĀ;Eᆨᆩ抐qual;抒nion;抔cr;쀀𝒮ar;拆ȀbcmpᇈᇛሉላĀ;sᇍᇎ拐etĀ;Eᇍᇕqual;抆ĀchᇠህeedsȀ;ESTᇭᇮᇴᇿ扻qual;檰lantEqual;扽ilde;承Tháྌ;我ƀ;esሒሓሣ拑rsetĀ;Eሜም抃qual;抇et»ሓրHRSacfhiorsሾቄ቉ቕ቞ቱቶኟዂወዑORN耻Þ䃞ADE;愢ĀHc቎ቒcy;䐋y;䐦Ābuቚቜ;䀉;䎤ƀaeyብቪቯron;䅤dil;䅢;䐢r;쀀𝔗Āeiቻ኉Dzኀ\0ኇefore;戴a;䎘Ācn኎ኘkSpace;쀀  Space;怉ldeȀ;EFTካኬኲኼ戼qual;扃ullEqual;扅ilde;扈pf;쀀𝕋ipleDot;惛Āctዖዛr;쀀𝒯rok;䅦ૡዷጎጚጦ\0ጬጱ\0\0\0\0\0ጸጽ፷ᎅ\0᏿ᐄᐊᐐĀcrዻጁute耻Ú䃚rĀ;oጇገ憟cir;楉rǣጓ\0጖y;䐎ve;䅬Āiyጞጣrc耻Û䃛;䐣blac;䅰r;쀀𝔘rave耻Ù䃙acr;䅪Ādiፁ፩erĀBPፈ፝Āarፍፐr;䁟acĀekፗፙ;揟et;掵arenthesis;揝onĀ;P፰፱拃lus;抎Āgp፻፿on;䅲f;쀀𝕌ЀADETadps᎕ᎮᎸᏄϨᏒᏗᏳrrowƀ;BDᅐᎠᎤar;椒ownArrow;懅ownArrow;憕quilibrium;楮eeĀ;AᏋᏌ报rrow;憥ownáϳerĀLRᏞᏨeftArrow;憖ightArrow;憗iĀ;lᏹᏺ䏒on;䎥ing;䅮cr;쀀𝒰ilde;䅨ml耻Ü䃜ҀDbcdefosvᐧᐬᐰᐳᐾᒅᒊᒐᒖash;披ar;櫫y;䐒ashĀ;lᐻᐼ抩;櫦Āerᑃᑅ;拁ƀbtyᑌᑐᑺar;怖Ā;iᑏᑕcalȀBLSTᑡᑥᑪᑴar;戣ine;䁼eparator;杘ilde;所ThinSpace;怊r;쀀𝔙pf;쀀𝕍cr;쀀𝒱dash;抪ʀcefosᒧᒬᒱᒶᒼirc;䅴dge;拀r;쀀𝔚pf;쀀𝕎cr;쀀𝒲Ȁfiosᓋᓐᓒᓘr;쀀𝔛;䎞pf;쀀𝕏cr;쀀𝒳ҀAIUacfosuᓱᓵᓹᓽᔄᔏᔔᔚᔠcy;䐯cy;䐇cy;䐮cute耻Ý䃝Āiyᔉᔍrc;䅶;䐫r;쀀𝔜pf;쀀𝕐cr;쀀𝒴ml;䅸ЀHacdefosᔵᔹᔿᕋᕏᕝᕠᕤcy;䐖cute;䅹Āayᕄᕉron;䅽;䐗ot;䅻Dzᕔ\0ᕛoWidtè૙a;䎖r;愨pf;愤cr;쀀𝒵௡ᖃᖊᖐ\0ᖰᖶᖿ\0\0\0\0ᗆᗛᗫᙟ᙭\0ᚕ᚛ᚲᚹ\0ᚾcute耻á䃡reve;䄃̀;Ediuyᖜᖝᖡᖣᖨᖭ戾;쀀∾̳;房rc耻â䃢te肻´̆;䐰lig耻æ䃦Ā;r²ᖺ;쀀𝔞rave耻à䃠ĀepᗊᗖĀfpᗏᗔsym;愵èᗓha;䎱ĀapᗟcĀclᗤᗧr;䄁g;樿ɤᗰ\0\0ᘊʀ;adsvᗺᗻᗿᘁᘇ戧nd;橕;橜lope;橘;橚΀;elmrszᘘᘙᘛᘞᘿᙏᙙ戠;榤e»ᘙsdĀ;aᘥᘦ戡ѡᘰᘲᘴᘶᘸᘺᘼᘾ;榨;榩;榪;榫;榬;榭;榮;榯tĀ;vᙅᙆ戟bĀ;dᙌᙍ抾;榝Āptᙔᙗh;戢»¹arr;捼Āgpᙣᙧon;䄅f;쀀𝕒΀;Eaeiop዁ᙻᙽᚂᚄᚇᚊ;橰cir;橯;扊d;手s;䀧roxĀ;e዁ᚒñᚃing耻å䃥ƀctyᚡᚦᚨr;쀀𝒶;䀪mpĀ;e዁ᚯñʈilde耻ã䃣ml耻ä䃤Āciᛂᛈoninôɲnt;樑ࠀNabcdefiklnoprsu᛭ᛱᜰ᜼ᝃᝈ᝸᝽០៦ᠹᡐᜍ᤽᥈ᥰot;櫭Ācrᛶ᜞kȀcepsᜀᜅᜍᜓong;扌psilon;䏶rime;怵imĀ;e᜚᜛戽q;拍Ŷᜢᜦee;抽edĀ;gᜬᜭ挅e»ᜭrkĀ;t፜᜷brk;掶Āoyᜁᝁ;䐱quo;怞ʀcmprtᝓ᝛ᝡᝤᝨausĀ;eĊĉptyv;榰séᜌnoõēƀahwᝯ᝱ᝳ;䎲;愶een;扬r;쀀𝔟g΀costuvwឍឝឳេ៕៛៞ƀaiuបពរðݠrc;旯p»፱ƀdptឤឨឭot;樀lus;樁imes;樂ɱឹ\0\0ើcup;樆ar;昅riangleĀdu៍្own;施p;斳plus;樄eåᑄåᒭarow;植ƀako៭ᠦᠵĀcn៲ᠣkƀlst៺֫᠂ozenge;槫riangleȀ;dlr᠒᠓᠘᠝斴own;斾eft;旂ight;斸k;搣Ʊᠫ\0ᠳƲᠯ\0ᠱ;斒;斑4;斓ck;斈ĀeoᠾᡍĀ;qᡃᡆ쀀=⃥uiv;쀀≡⃥t;挐Ȁptwxᡙᡞᡧᡬf;쀀𝕓Ā;tᏋᡣom»Ꮜtie;拈؀DHUVbdhmptuvᢅᢖᢪᢻᣗᣛᣬ᣿ᤅᤊᤐᤡȀLRlrᢎᢐᢒᢔ;敗;敔;敖;敓ʀ;DUduᢡᢢᢤᢦᢨ敐;敦;敩;敤;敧ȀLRlrᢳᢵᢷᢹ;敝;敚;敜;教΀;HLRhlrᣊᣋᣍᣏᣑᣓᣕ救;敬;散;敠;敫;敢;敟ox;槉ȀLRlrᣤᣦᣨᣪ;敕;敒;攐;攌ʀ;DUduڽ᣷᣹᣻᣽;敥;敨;攬;攴inus;抟lus;択imes;抠ȀLRlrᤙᤛᤝ᤟;敛;敘;攘;攔΀;HLRhlrᤰᤱᤳᤵᤷ᤻᤹攂;敪;敡;敞;攼;攤;攜Āevģ᥂bar耻¦䂦Ȁceioᥑᥖᥚᥠr;쀀𝒷mi;恏mĀ;e᜚᜜lƀ;bhᥨᥩᥫ䁜;槅sub;柈Ŭᥴ᥾lĀ;e᥹᥺怢t»᥺pƀ;Eeįᦅᦇ;檮Ā;qۜۛೡᦧ\0᧨ᨑᨕᨲ\0ᨷᩐ\0\0᪴\0\0᫁\0\0ᬡᬮ᭍᭒\0᯽\0ᰌƀcpr᦭ᦲ᧝ute;䄇̀;abcdsᦿᧀᧄ᧊᧕᧙戩nd;橄rcup;橉Āau᧏᧒p;橋p;橇ot;橀;쀀∩︀Āeo᧢᧥t;恁îړȀaeiu᧰᧻ᨁᨅǰ᧵\0᧸s;橍on;䄍dil耻ç䃧rc;䄉psĀ;sᨌᨍ橌m;橐ot;䄋ƀdmnᨛᨠᨦil肻¸ƭptyv;榲t脀¢;eᨭᨮ䂢räƲr;쀀𝔠ƀceiᨽᩀᩍy;䑇ckĀ;mᩇᩈ朓ark»ᩈ;䏇r΀;Ecefms᩟᩠ᩢᩫ᪤᪪᪮旋;槃ƀ;elᩩᩪᩭ䋆q;扗eɡᩴ\0\0᪈rrowĀlr᩼᪁eft;憺ight;憻ʀRSacd᪒᪔᪖᪚᪟»ཇ;擈st;抛irc;抚ash;抝nint;樐id;櫯cir;槂ubsĀ;u᪻᪼晣it»᪼ˬ᫇᫔᫺\0ᬊonĀ;eᫍᫎ䀺Ā;qÇÆɭ᫙\0\0᫢aĀ;t᫞᫟䀬;䁀ƀ;fl᫨᫩᫫戁îᅠeĀmx᫱᫶ent»᫩eóɍǧ᫾\0ᬇĀ;dኻᬂot;橭nôɆƀfryᬐᬔᬗ;쀀𝕔oäɔ脀©;sŕᬝr;愗Āaoᬥᬩrr;憵ss;朗Ācuᬲᬷr;쀀𝒸Ābpᬼ᭄Ā;eᭁᭂ櫏;櫑Ā;eᭉᭊ櫐;櫒dot;拯΀delprvw᭠᭬᭷ᮂᮬᯔ᯹arrĀlr᭨᭪;椸;椵ɰ᭲\0\0᭵r;拞c;拟arrĀ;p᭿ᮀ憶;椽̀;bcdosᮏᮐᮖᮡᮥᮨ截rcap;橈Āauᮛᮞp;橆p;橊ot;抍r;橅;쀀∪︀Ȁalrv᮵ᮿᯞᯣrrĀ;mᮼᮽ憷;椼yƀevwᯇᯔᯘqɰᯎ\0\0ᯒreã᭳uã᭵ee;拎edge;拏en耻¤䂤earrowĀlrᯮ᯳eft»ᮀight»ᮽeäᯝĀciᰁᰇoninôǷnt;戱lcty;挭ঀAHabcdefhijlorstuwz᰸᰻᰿ᱝᱩᱵᲊᲞᲬᲷ᳻᳿ᴍᵻᶑᶫᶻ᷆᷍rò΁ar;楥Ȁglrs᱈ᱍ᱒᱔ger;怠eth;愸òᄳhĀ;vᱚᱛ怐»ऊūᱡᱧarow;椏aã̕Āayᱮᱳron;䄏;䐴ƀ;ao̲ᱼᲄĀgrʿᲁr;懊tseq;橷ƀglmᲑᲔᲘ耻°䂰ta;䎴ptyv;榱ĀirᲣᲨsht;楿;쀀𝔡arĀlrᲳᲵ»ࣜ»သʀaegsv᳂͸᳖᳜᳠mƀ;oș᳊᳔ndĀ;ș᳑uit;晦amma;䏝in;拲ƀ;io᳧᳨᳸䃷de脀÷;o᳧ᳰntimes;拇nø᳷cy;䑒cɯᴆ\0\0ᴊrn;挞op;挍ʀlptuwᴘᴝᴢᵉᵕlar;䀤f;쀀𝕕ʀ;emps̋ᴭᴷᴽᵂqĀ;d͒ᴳot;扑inus;戸lus;戔quare;抡blebarwedgåúnƀadhᄮᵝᵧownarrowóᲃarpoonĀlrᵲᵶefôᲴighôᲶŢᵿᶅkaro÷གɯᶊ\0\0ᶎrn;挟op;挌ƀcotᶘᶣᶦĀryᶝᶡ;쀀𝒹;䑕l;槶rok;䄑Ādrᶰᶴot;拱iĀ;fᶺ᠖斿Āah᷀᷃ròЩaòྦangle;榦Āci᷒ᷕy;䑟grarr;柿ऀDacdefglmnopqrstuxḁḉḙḸոḼṉṡṾấắẽỡἪἷὄ὎὚ĀDoḆᴴoôᲉĀcsḎḔute耻é䃩ter;橮ȀaioyḢḧḱḶron;䄛rĀ;cḭḮ扖耻ê䃪lon;払;䑍ot;䄗ĀDrṁṅot;扒;쀀𝔢ƀ;rsṐṑṗ檚ave耻è䃨Ā;dṜṝ檖ot;檘Ȁ;ilsṪṫṲṴ檙nters;揧;愓Ā;dṹṺ檕ot;檗ƀapsẅẉẗcr;䄓tyƀ;svẒẓẕ戅et»ẓpĀ1;ẝẤijạả;怄;怅怃ĀgsẪẬ;䅋p;怂ĀgpẴẸon;䄙f;쀀𝕖ƀalsỄỎỒrĀ;sỊị拕l;槣us;橱iƀ;lvỚớở䎵on»ớ;䏵ȀcsuvỪỳἋἣĀioữḱrc»Ḯɩỹ\0\0ỻíՈantĀglἂἆtr»ṝess»Ṻƀaeiἒ἖Ἒls;䀽st;扟vĀ;DȵἠD;橸parsl;槥ĀDaἯἳot;打rr;楱ƀcdiἾὁỸr;愯oô͒ĀahὉὋ;䎷耻ð䃰Āmrὓὗl耻ë䃫o;悬ƀcipὡὤὧl;䀡sôծĀeoὬὴctatioîՙnentialåչৡᾒ\0ᾞ\0ᾡᾧ\0\0ῆῌ\0ΐ\0ῦῪ \0 ⁚llingdotseñṄy;䑄male;晀ƀilrᾭᾳ῁lig;耀ffiɩᾹ\0\0᾽g;耀ffig;耀ffl;쀀𝔣lig;耀filig;쀀fjƀaltῙ῜ῡt;晭ig;耀flns;斱of;䆒ǰ΅\0ῳf;쀀𝕗ĀakֿῷĀ;vῼ´拔;櫙artint;樍Āao‌⁕Ācs‑⁒ႉ‸⁅⁈\0⁐β•‥‧‪‬\0‮耻½䂽;慓耻¼䂼;慕;慙;慛Ƴ‴\0‶;慔;慖ʴ‾⁁\0\0⁃耻¾䂾;慗;慜5;慘ƶ⁌\0⁎;慚;慝8;慞l;恄wn;挢cr;쀀𝒻ࢀEabcdefgijlnorstv₂₉₟₥₰₴⃰⃵⃺⃿℃ℒℸ̗ℾ⅒↞Ā;lٍ₇;檌ƀcmpₐₕ₝ute;䇵maĀ;dₜ᳚䎳;檆reve;䄟Āiy₪₮rc;䄝;䐳ot;䄡Ȁ;lqsؾق₽⃉ƀ;qsؾٌ⃄lanô٥Ȁ;cdl٥⃒⃥⃕c;檩otĀ;o⃜⃝檀Ā;l⃢⃣檂;檄Ā;e⃪⃭쀀⋛︀s;檔r;쀀𝔤Ā;gٳ؛mel;愷cy;䑓Ȁ;Eajٚℌℎℐ;檒;檥;檤ȀEaesℛℝ℩ℴ;扩pĀ;p℣ℤ檊rox»ℤĀ;q℮ℯ檈Ā;q℮ℛim;拧pf;쀀𝕘Āci⅃ⅆr;愊mƀ;el٫ⅎ⅐;檎;檐茀>;cdlqr׮ⅠⅪⅮⅳⅹĀciⅥⅧ;檧r;橺ot;拗Par;榕uest;橼ʀadelsↄⅪ←ٖ↛ǰ↉\0↎proø₞r;楸qĀlqؿ↖lesó₈ií٫Āen↣↭rtneqq;쀀≩︀Å↪ԀAabcefkosy⇄⇇⇱⇵⇺∘∝∯≨≽ròΠȀilmr⇐⇔⇗⇛rsðᒄf»․ilôکĀdr⇠⇤cy;䑊ƀ;cwࣴ⇫⇯ir;楈;憭ar;意irc;䄥ƀalr∁∎∓rtsĀ;u∉∊晥it»∊lip;怦con;抹r;쀀𝔥sĀew∣∩arow;椥arow;椦ʀamopr∺∾≃≞≣rr;懿tht;戻kĀlr≉≓eftarrow;憩ightarrow;憪f;쀀𝕙bar;怕ƀclt≯≴≸r;쀀𝒽asè⇴rok;䄧Ābp⊂⊇ull;恃hen»ᱛૡ⊣\0⊪\0⊸⋅⋎\0⋕⋳\0\0⋸⌢⍧⍢⍿\0⎆⎪⎴cute耻í䃭ƀ;iyݱ⊰⊵rc耻î䃮;䐸Ācx⊼⊿y;䐵cl耻¡䂡ĀfrΟ⋉;쀀𝔦rave耻ì䃬Ȁ;inoܾ⋝⋩⋮Āin⋢⋦nt;樌t;戭fin;槜ta;愩lig;䄳ƀaop⋾⌚⌝ƀcgt⌅⌈⌗r;䄫ƀelpܟ⌏⌓inåގarôܠh;䄱f;抷ed;䆵ʀ;cfotӴ⌬⌱⌽⍁are;愅inĀ;t⌸⌹戞ie;槝doô⌙ʀ;celpݗ⍌⍐⍛⍡al;抺Āgr⍕⍙eróᕣã⍍arhk;樗rod;樼Ȁcgpt⍯⍲⍶⍻y;䑑on;䄯f;쀀𝕚a;䎹uest耻¿䂿Āci⎊⎏r;쀀𝒾nʀ;EdsvӴ⎛⎝⎡ӳ;拹ot;拵Ā;v⎦⎧拴;拳Ā;iݷ⎮lde;䄩ǫ⎸\0⎼cy;䑖l耻ï䃯̀cfmosu⏌⏗⏜⏡⏧⏵Āiy⏑⏕rc;䄵;䐹r;쀀𝔧ath;䈷pf;쀀𝕛ǣ⏬\0⏱r;쀀𝒿rcy;䑘kcy;䑔Ѐacfghjos␋␖␢␧␭␱␵␻ppaĀ;v␓␔䎺;䏰Āey␛␠dil;䄷;䐺r;쀀𝔨reen;䄸cy;䑅cy;䑜pf;쀀𝕜cr;쀀𝓀஀ABEHabcdefghjlmnoprstuv⑰⒁⒆⒍⒑┎┽╚▀♎♞♥♹♽⚚⚲⛘❝❨➋⟀⠁⠒ƀart⑷⑺⑼rò৆òΕail;椛arr;椎Ā;gঔ⒋;檋ar;楢ॣ⒥\0⒪\0⒱\0\0\0\0\0⒵Ⓔ\0ⓆⓈⓍ\0⓹ute;䄺mptyv;榴raîࡌbda;䎻gƀ;dlࢎⓁⓃ;榑åࢎ;檅uo耻«䂫rЀ;bfhlpst࢙ⓞⓦⓩ⓫⓮⓱⓵Ā;f࢝ⓣs;椟s;椝ë≒p;憫l;椹im;楳l;憢ƀ;ae⓿─┄檫il;椙Ā;s┉┊檭;쀀⪭︀ƀabr┕┙┝rr;椌rk;杲Āak┢┬cĀek┨┪;䁻;䁛Āes┱┳;榋lĀdu┹┻;榏;榍Ȁaeuy╆╋╖╘ron;䄾Ādi═╔il;䄼ìࢰâ┩;䐻Ȁcqrs╣╦╭╽a;椶uoĀ;rนᝆĀdu╲╷har;楧shar;楋h;憲ʀ;fgqs▋▌উ◳◿扤tʀahlrt▘▤▷◂◨rrowĀ;t࢙□aé⓶arpoonĀdu▯▴own»њp»०eftarrows;懇ightƀahs◍◖◞rrowĀ;sࣴࢧarpoonó྘quigarro÷⇰hreetimes;拋ƀ;qs▋ও◺lanôবʀ;cdgsব☊☍☝☨c;檨otĀ;o☔☕橿Ā;r☚☛檁;檃Ā;e☢☥쀀⋚︀s;檓ʀadegs☳☹☽♉♋pproøⓆot;拖qĀgq♃♅ôউgtò⒌ôছiíলƀilr♕࣡♚sht;楼;쀀𝔩Ā;Eজ♣;檑š♩♶rĀdu▲♮Ā;l॥♳;楪lk;斄cy;䑙ʀ;achtੈ⚈⚋⚑⚖rò◁orneòᴈard;楫ri;旺Āio⚟⚤dot;䅀ustĀ;a⚬⚭掰che»⚭ȀEaes⚻⚽⛉⛔;扨pĀ;p⛃⛄檉rox»⛄Ā;q⛎⛏檇Ā;q⛎⚻im;拦Ѐabnoptwz⛩⛴⛷✚✯❁❇❐Ānr⛮⛱g;柬r;懽rëࣁgƀlmr⛿✍✔eftĀar০✇ightá৲apsto;柼ightá৽parrowĀlr✥✩efô⓭ight;憬ƀafl✶✹✽r;榅;쀀𝕝us;樭imes;樴š❋❏st;戗áፎƀ;ef❗❘᠀旊nge»❘arĀ;l❤❥䀨t;榓ʀachmt❳❶❼➅➇ròࢨorneòᶌarĀ;d྘➃;業;怎ri;抿̀achiqt➘➝ੀ➢➮➻quo;怹r;쀀𝓁mƀ;egল➪➬;檍;檏Ābu┪➳oĀ;rฟ➹;怚rok;䅂萀<;cdhilqrࠫ⟒☹⟜⟠⟥⟪⟰Āci⟗⟙;檦r;橹reå◲mes;拉arr;楶uest;橻ĀPi⟵⟹ar;榖ƀ;ef⠀भ᠛旃rĀdu⠇⠍shar;楊har;楦Āen⠗⠡rtneqq;쀀≨︀Å⠞܀Dacdefhilnopsu⡀⡅⢂⢎⢓⢠⢥⢨⣚⣢⣤ઃ⣳⤂Dot;戺Ȁclpr⡎⡒⡣⡽r耻¯䂯Āet⡗⡙;時Ā;e⡞⡟朠se»⡟Ā;sျ⡨toȀ;dluျ⡳⡷⡻owîҌefôएðᏑker;斮Āoy⢇⢌mma;権;䐼ash;怔asuredangle»ᘦr;쀀𝔪o;愧ƀcdn⢯⢴⣉ro耻µ䂵Ȁ;acdᑤ⢽⣀⣄sôᚧir;櫰ot肻·Ƶusƀ;bd⣒ᤃ⣓戒Ā;uᴼ⣘;横ţ⣞⣡p;櫛ò−ðઁĀdp⣩⣮els;抧f;쀀𝕞Āct⣸⣽r;쀀𝓂pos»ᖝƀ;lm⤉⤊⤍䎼timap;抸ఀGLRVabcdefghijlmoprstuvw⥂⥓⥾⦉⦘⧚⧩⨕⨚⩘⩝⪃⪕⪤⪨⬄⬇⭄⭿⮮ⰴⱧⱼ⳩Āgt⥇⥋;쀀⋙̸Ā;v⥐௏쀀≫⃒ƀelt⥚⥲⥶ftĀar⥡⥧rrow;懍ightarrow;懎;쀀⋘̸Ā;v⥻ే쀀≪⃒ightarrow;懏ĀDd⦎⦓ash;抯ash;抮ʀbcnpt⦣⦧⦬⦱⧌la»˞ute;䅄g;쀀∠⃒ʀ;Eiop඄⦼⧀⧅⧈;쀀⩰̸d;쀀≋̸s;䅉roø඄urĀ;a⧓⧔普lĀ;s⧓ସdz⧟\0⧣p肻 ଷmpĀ;e௹ఀʀaeouy⧴⧾⨃⨐⨓ǰ⧹\0⧻;橃on;䅈dil;䅆ngĀ;dൾ⨊ot;쀀⩭̸p;橂;䐽ash;怓΀;Aadqsxஒ⨩⨭⨻⩁⩅⩐rr;懗rĀhr⨳⨶k;椤Ā;oᏲᏰot;쀀≐̸uiöୣĀei⩊⩎ar;椨í஘istĀ;s஠டr;쀀𝔫ȀEest௅⩦⩹⩼ƀ;qs஼⩭௡ƀ;qs஼௅⩴lanô௢ií௪Ā;rஶ⪁»ஷƀAap⪊⪍⪑rò⥱rr;憮ar;櫲ƀ;svྍ⪜ྌĀ;d⪡⪢拼;拺cy;䑚΀AEadest⪷⪺⪾⫂⫅⫶⫹rò⥦;쀀≦̸rr;憚r;急Ȁ;fqs఻⫎⫣⫯tĀar⫔⫙rro÷⫁ightarro÷⪐ƀ;qs఻⪺⫪lanôౕĀ;sౕ⫴»శiíౝĀ;rవ⫾iĀ;eచథiäඐĀpt⬌⬑f;쀀𝕟膀¬;in⬙⬚⬶䂬nȀ;Edvஉ⬤⬨⬮;쀀⋹̸ot;쀀⋵̸ǡஉ⬳⬵;拷;拶iĀ;vಸ⬼ǡಸ⭁⭃;拾;拽ƀaor⭋⭣⭩rȀ;ast୻⭕⭚⭟lleì୻l;쀀⫽⃥;쀀∂̸lint;樔ƀ;ceಒ⭰⭳uåಥĀ;cಘ⭸Ā;eಒ⭽ñಘȀAait⮈⮋⮝⮧rò⦈rrƀ;cw⮔⮕⮙憛;쀀⤳̸;쀀↝̸ghtarrow»⮕riĀ;eೋೖ΀chimpqu⮽⯍⯙⬄୸⯤⯯Ȁ;cerല⯆ഷ⯉uå൅;쀀𝓃ortɭ⬅\0\0⯖ará⭖mĀ;e൮⯟Ā;q൴൳suĀbp⯫⯭å೸åഋƀbcp⯶ⰑⰙȀ;Ees⯿ⰀഢⰄ抄;쀀⫅̸etĀ;eഛⰋqĀ;qണⰀcĀ;eലⰗñസȀ;EesⰢⰣൟⰧ抅;쀀⫆̸etĀ;e൘ⰮqĀ;qൠⰣȀgilrⰽⰿⱅⱇìௗlde耻ñ䃱çృiangleĀlrⱒⱜeftĀ;eచⱚñదightĀ;eೋⱥñ೗Ā;mⱬⱭ䎽ƀ;esⱴⱵⱹ䀣ro;愖p;怇ҀDHadgilrsⲏⲔⲙⲞⲣⲰⲶⳓⳣash;抭arr;椄p;쀀≍⃒ash;抬ĀetⲨⲬ;쀀≥⃒;쀀>⃒nfin;槞ƀAetⲽⳁⳅrr;椂;쀀≤⃒Ā;rⳊⳍ쀀<⃒ie;쀀⊴⃒ĀAtⳘⳜrr;椃rie;쀀⊵⃒im;쀀∼⃒ƀAan⳰⳴ⴂrr;懖rĀhr⳺⳽k;椣Ā;oᏧᏥear;椧ቓ᪕\0\0\0\0\0\0\0\0\0\0\0\0\0ⴭ\0ⴸⵈⵠⵥ⵲ⶄᬇ\0\0ⶍⶫ\0ⷈⷎ\0ⷜ⸙⸫⸾⹃Ācsⴱ᪗ute耻ó䃳ĀiyⴼⵅrĀ;c᪞ⵂ耻ô䃴;䐾ʀabios᪠ⵒⵗLjⵚlac;䅑v;樸old;榼lig;䅓Ācr⵩⵭ir;榿;쀀𝔬ͯ⵹\0\0⵼\0ⶂn;䋛ave耻ò䃲;槁Ābmⶈ෴ar;榵Ȁacitⶕ⶘ⶥⶨrò᪀Āir⶝ⶠr;榾oss;榻nå๒;槀ƀaeiⶱⶵⶹcr;䅍ga;䏉ƀcdnⷀⷅǍron;䎿;榶pf;쀀𝕠ƀaelⷔ⷗ǒr;榷rp;榹΀;adiosvⷪⷫⷮ⸈⸍⸐⸖戨rò᪆Ȁ;efmⷷⷸ⸂⸅橝rĀ;oⷾⷿ愴f»ⷿ耻ª䂪耻º䂺gof;抶r;橖lope;橗;橛ƀclo⸟⸡⸧ò⸁ash耻ø䃸l;折iŬⸯ⸴de耻õ䃵esĀ;aǛ⸺s;樶ml耻ö䃶bar;挽ૡ⹞\0⹽\0⺀⺝\0⺢⺹\0\0⻋ຜ\0⼓\0\0⼫⾼\0⿈rȀ;astЃ⹧⹲຅脀¶;l⹭⹮䂶leìЃɩ⹸\0\0⹻m;櫳;櫽y;䐿rʀcimpt⺋⺏⺓ᡥ⺗nt;䀥od;䀮il;怰enk;怱r;쀀𝔭ƀimo⺨⺰⺴Ā;v⺭⺮䏆;䏕maô੶ne;明ƀ;tv⺿⻀⻈䏀chfork»´;䏖Āau⻏⻟nĀck⻕⻝kĀ;h⇴⻛;愎ö⇴sҀ;abcdemst⻳⻴ᤈ⻹⻽⼄⼆⼊⼎䀫cir;樣ir;樢Āouᵀ⼂;樥;橲n肻±ຝim;樦wo;樧ƀipu⼙⼠⼥ntint;樕f;쀀𝕡nd耻£䂣Ԁ;Eaceinosu່⼿⽁⽄⽇⾁⾉⾒⽾⾶;檳p;檷uå໙Ā;c໎⽌̀;acens່⽙⽟⽦⽨⽾pproø⽃urlyeñ໙ñ໎ƀaes⽯⽶⽺pprox;檹qq;檵im;拨iíໟmeĀ;s⾈ຮ怲ƀEas⽸⾐⽺ð⽵ƀdfp໬⾙⾯ƀals⾠⾥⾪lar;挮ine;挒urf;挓Ā;t໻⾴ï໻rel;抰Āci⿀⿅r;쀀𝓅;䏈ncsp;怈̀fiopsu⿚⋢⿟⿥⿫⿱r;쀀𝔮pf;쀀𝕢rime;恗cr;쀀𝓆ƀaeo⿸〉〓tĀei⿾々rnionóڰnt;樖stĀ;e【】䀿ñἙô༔઀ABHabcdefhilmnoprstux぀けさすムㄎㄫㅇㅢㅲㆎ㈆㈕㈤㈩㉘㉮㉲㊐㊰㊷ƀartぇおがròႳòϝail;検aròᱥar;楤΀cdenqrtとふへみわゔヌĀeuねぱ;쀀∽̱te;䅕iãᅮmptyv;榳gȀ;del࿑らるろ;榒;榥å࿑uo耻»䂻rր;abcfhlpstw࿜ガクシスゼゾダッデナp;極Ā;f࿠ゴs;椠;椳s;椞ë≝ð✮l;楅im;楴l;憣;憝Āaiパフil;椚oĀ;nホボ戶aló༞ƀabrョリヮrò៥rk;杳ĀakンヽcĀekヹ・;䁽;䁝Āes㄂㄄;榌lĀduㄊㄌ;榎;榐Ȁaeuyㄗㄜㄧㄩron;䅙Ādiㄡㄥil;䅗ì࿲âヺ;䑀Ȁclqsㄴㄷㄽㅄa;椷dhar;楩uoĀ;rȎȍh;憳ƀacgㅎㅟངlȀ;ipsླྀㅘㅛႜnåႻarôྩt;断ƀilrㅩဣㅮsht;楽;쀀𝔯ĀaoㅷㆆrĀduㅽㅿ»ѻĀ;l႑ㆄ;楬Ā;vㆋㆌ䏁;䏱ƀgns㆕ㇹㇼht̀ahlrstㆤㆰ㇂㇘㇤㇮rrowĀ;t࿜ㆭaéトarpoonĀduㆻㆿowîㅾp»႒eftĀah㇊㇐rrowó࿪arpoonóՑightarrows;應quigarro÷ニhreetimes;拌g;䋚ingdotseñἲƀahm㈍㈐㈓rò࿪aòՑ;怏oustĀ;a㈞㈟掱che»㈟mid;櫮Ȁabpt㈲㈽㉀㉒Ānr㈷㈺g;柭r;懾rëဃƀafl㉇㉊㉎r;榆;쀀𝕣us;樮imes;樵Āap㉝㉧rĀ;g㉣㉤䀩t;榔olint;樒arò㇣Ȁachq㉻㊀Ⴜ㊅quo;怺r;쀀𝓇Ābu・㊊oĀ;rȔȓƀhir㊗㊛㊠reåㇸmes;拊iȀ;efl㊪ၙᠡ㊫方tri;槎luhar;楨;愞ൡ㋕㋛㋟㌬㌸㍱\0㍺㎤\0\0㏬㏰\0㐨㑈㑚㒭㒱㓊㓱\0㘖\0\0㘳cute;䅛quï➺Ԁ;Eaceinpsyᇭ㋳㋵㋿㌂㌋㌏㌟㌦㌩;檴ǰ㋺\0㋼;檸on;䅡uåᇾĀ;dᇳ㌇il;䅟rc;䅝ƀEas㌖㌘㌛;檶p;檺im;择olint;樓iíሄ;䑁otƀ;be㌴ᵇ㌵担;橦΀Aacmstx㍆㍊㍗㍛㍞㍣㍭rr;懘rĀhr㍐㍒ë∨Ā;oਸ਼਴t耻§䂧i;䀻war;椩mĀin㍩ðnuóñt;朶rĀ;o㍶⁕쀀𝔰Ȁacoy㎂㎆㎑㎠rp;景Āhy㎋㎏cy;䑉;䑈rtɭ㎙\0\0㎜iäᑤaraì⹯耻­䂭Āgm㎨㎴maƀ;fv㎱㎲㎲䏃;䏂Ѐ;deglnprካ㏅㏉㏎㏖㏞㏡㏦ot;橪Ā;q኱ኰĀ;E㏓㏔檞;檠Ā;E㏛㏜檝;檟e;扆lus;樤arr;楲aròᄽȀaeit㏸㐈㐏㐗Āls㏽㐄lsetmé㍪hp;樳parsl;槤Ādlᑣ㐔e;挣Ā;e㐜㐝檪Ā;s㐢㐣檬;쀀⪬︀ƀflp㐮㐳㑂tcy;䑌Ā;b㐸㐹䀯Ā;a㐾㐿槄r;挿f;쀀𝕤aĀdr㑍ЂesĀ;u㑔㑕晠it»㑕ƀcsu㑠㑹㒟Āau㑥㑯pĀ;sᆈ㑫;쀀⊓︀pĀ;sᆴ㑵;쀀⊔︀uĀbp㑿㒏ƀ;esᆗᆜ㒆etĀ;eᆗ㒍ñᆝƀ;esᆨᆭ㒖etĀ;eᆨ㒝ñᆮƀ;afᅻ㒦ְrť㒫ֱ»ᅼaròᅈȀcemt㒹㒾㓂㓅r;쀀𝓈tmîñiì㐕aræᆾĀar㓎㓕rĀ;f㓔ឿ昆Āan㓚㓭ightĀep㓣㓪psiloîỠhé⺯s»⡒ʀbcmnp㓻㕞ሉ㖋㖎Ҁ;Edemnprs㔎㔏㔑㔕㔞㔣㔬㔱㔶抂;櫅ot;檽Ā;dᇚ㔚ot;櫃ult;櫁ĀEe㔨㔪;櫋;把lus;檿arr;楹ƀeiu㔽㕒㕕tƀ;en㔎㕅㕋qĀ;qᇚ㔏eqĀ;q㔫㔨m;櫇Ābp㕚㕜;櫕;櫓c̀;acensᇭ㕬㕲㕹㕻㌦pproø㋺urlyeñᇾñᇳƀaes㖂㖈㌛pproø㌚qñ㌗g;晪ڀ123;Edehlmnps㖩㖬㖯ሜ㖲㖴㗀㗉㗕㗚㗟㗨㗭耻¹䂹耻²䂲耻³䂳;櫆Āos㖹㖼t;檾ub;櫘Ā;dሢ㗅ot;櫄sĀou㗏㗒l;柉b;櫗arr;楻ult;櫂ĀEe㗤㗦;櫌;抋lus;櫀ƀeiu㗴㘉㘌tƀ;enሜ㗼㘂qĀ;qሢ㖲eqĀ;q㗧㗤m;櫈Ābp㘑㘓;櫔;櫖ƀAan㘜㘠㘭rr;懙rĀhr㘦㘨ë∮Ā;oਫ਩war;椪lig耻ß䃟௡㙑㙝㙠ዎ㙳㙹\0㙾㛂\0\0\0\0\0㛛㜃\0㜉㝬\0\0\0㞇ɲ㙖\0\0㙛get;挖;䏄rë๟ƀaey㙦㙫㙰ron;䅥dil;䅣;䑂lrec;挕r;쀀𝔱Ȁeiko㚆㚝㚵㚼Dz㚋\0㚑eĀ4fኄኁaƀ;sv㚘㚙㚛䎸ym;䏑Ācn㚢㚲kĀas㚨㚮pproø዁im»ኬsðኞĀas㚺㚮ð዁rn耻þ䃾Ǭ̟㛆⋧es膀×;bd㛏㛐㛘䃗Ā;aᤏ㛕r;樱;樰ƀeps㛡㛣㜀á⩍Ȁ;bcf҆㛬㛰㛴ot;挶ir;櫱Ā;o㛹㛼쀀𝕥rk;櫚á㍢rime;怴ƀaip㜏㜒㝤dåቈ΀adempst㜡㝍㝀㝑㝗㝜㝟ngleʀ;dlqr㜰㜱㜶㝀㝂斵own»ᶻeftĀ;e⠀㜾ñम;扜ightĀ;e㊪㝋ñၚot;旬inus;樺lus;樹b;槍ime;樻ezium;揢ƀcht㝲㝽㞁Āry㝷㝻;쀀𝓉;䑆cy;䑛rok;䅧Āio㞋㞎xô᝷headĀlr㞗㞠eftarro÷ࡏightarrow»ཝऀAHabcdfghlmoprstuw㟐㟓㟗㟤㟰㟼㠎㠜㠣㠴㡑㡝㡫㢩㣌㣒㣪㣶ròϭar;楣Ācr㟜㟢ute耻ú䃺òᅐrǣ㟪\0㟭y;䑞ve;䅭Āiy㟵㟺rc耻û䃻;䑃ƀabh㠃㠆㠋ròᎭlac;䅱aòᏃĀir㠓㠘sht;楾;쀀𝔲rave耻ù䃹š㠧㠱rĀlr㠬㠮»ॗ»ႃlk;斀Āct㠹㡍ɯ㠿\0\0㡊rnĀ;e㡅㡆挜r»㡆op;挏ri;旸Āal㡖㡚cr;䅫肻¨͉Āgp㡢㡦on;䅳f;쀀𝕦̀adhlsuᅋ㡸㡽፲㢑㢠ownáᎳarpoonĀlr㢈㢌efô㠭ighô㠯iƀ;hl㢙㢚㢜䏅»ᏺon»㢚parrows;懈ƀcit㢰㣄㣈ɯ㢶\0\0㣁rnĀ;e㢼㢽挝r»㢽op;挎ng;䅯ri;旹cr;쀀𝓊ƀdir㣙㣝㣢ot;拰lde;䅩iĀ;f㜰㣨»᠓Āam㣯㣲rò㢨l耻ü䃼angle;榧ހABDacdeflnoprsz㤜㤟㤩㤭㦵㦸㦽㧟㧤㧨㧳㧹㧽㨁㨠ròϷarĀ;v㤦㤧櫨;櫩asèϡĀnr㤲㤷grt;榜΀eknprst㓣㥆㥋㥒㥝㥤㦖appá␕othinçẖƀhir㓫⻈㥙opô⾵Ā;hᎷ㥢ïㆍĀiu㥩㥭gmá㎳Ābp㥲㦄setneqĀ;q㥽㦀쀀⊊︀;쀀⫋︀setneqĀ;q㦏㦒쀀⊋︀;쀀⫌︀Āhr㦛㦟etá㚜iangleĀlr㦪㦯eft»थight»ၑy;䐲ash»ံƀelr㧄㧒㧗ƀ;beⷪ㧋㧏ar;抻q;扚lip;拮Ābt㧜ᑨaòᑩr;쀀𝔳tré㦮suĀbp㧯㧱»ജ»൙pf;쀀𝕧roð໻tré㦴Ācu㨆㨋r;쀀𝓋Ābp㨐㨘nĀEe㦀㨖»㥾nĀEe㦒㨞»㦐igzag;榚΀cefoprs㨶㨻㩖㩛㩔㩡㩪irc;䅵Ādi㩀㩑Ābg㩅㩉ar;機eĀ;qᗺ㩏;扙erp;愘r;쀀𝔴pf;쀀𝕨Ā;eᑹ㩦atèᑹcr;쀀𝓌ૣណ㪇\0㪋\0㪐㪛\0\0㪝㪨㪫㪯\0\0㫃㫎\0㫘ៜ៟tré៑r;쀀𝔵ĀAa㪔㪗ròσrò৶;䎾ĀAa㪡㪤ròθrò৫að✓is;拻ƀdptឤ㪵㪾Āfl㪺ឩ;쀀𝕩imåឲĀAa㫇㫊ròώròਁĀcq㫒ីr;쀀𝓍Āpt៖㫜ré។Ѐacefiosu㫰㫽㬈㬌㬑㬕㬛㬡cĀuy㫶㫻te耻ý䃽;䑏Āiy㬂㬆rc;䅷;䑋n耻¥䂥r;쀀𝔶cy;䑗pf;쀀𝕪cr;쀀𝓎Ācm㬦㬩y;䑎l耻ÿ䃿Ԁacdefhiosw㭂㭈㭔㭘㭤㭩㭭㭴㭺㮀cute;䅺Āay㭍㭒ron;䅾;䐷ot;䅼Āet㭝㭡træᕟa;䎶r;쀀𝔷cy;䐶grarr;懝pf;쀀𝕫cr;쀀𝓏Ājn㮅㮇;怍j;怌'.split("").map(e=>e.charCodeAt(0))),Er=new Uint16Array("Ȁaglq \x1Bɭ\0\0p;䀦os;䀧t;䀾t;䀼uot;䀢".split("").map(e=>e.charCodeAt(0)));var du;const Cr=new Map([[0,65533],[128,8364],[130,8218],[131,402],[132,8222],[133,8230],[134,8224],[135,8225],[136,710],[137,8240],[138,352],[139,8249],[140,338],[142,381],[145,8216],[146,8217],[147,8220],[148,8221],[149,8226],[150,8211],[151,8212],[152,732],[153,8482],[154,353],[155,8250],[156,339],[158,382],[159,376]]),wr=(du=String.fromCodePoint)!==null&&du!==void 0?du:function(e){let u="";return e>65535&&(e-=65536,u+=String.fromCharCode(e>>>10&1023|55296),e=56320|e&1023),u+=String.fromCharCode(e),u};function Ar(e){var u;return e>=55296&&e<=57343||e>1114111?65533:(u=Cr.get(e))!==null&&u!==void 0?u:e}var V;(function(e){e[e.NUM=35]="NUM",e[e.SEMI=59]="SEMI",e[e.EQUALS=61]="EQUALS",e[e.ZERO=48]="ZERO",e[e.NINE=57]="NINE",e[e.LOWER_A=97]="LOWER_A",e[e.LOWER_F=102]="LOWER_F",e[e.LOWER_X=120]="LOWER_X",e[e.LOWER_Z=122]="LOWER_Z",e[e.UPPER_A=65]="UPPER_A",e[e.UPPER_F=70]="UPPER_F",e[e.UPPER_Z=90]="UPPER_Z"})(V||(V={}));const Dr=32;var xe;(function(e){e[e.VALUE_LENGTH=49152]="VALUE_LENGTH",e[e.BRANCH_LENGTH=16256]="BRANCH_LENGTH",e[e.JUMP_TABLE=127]="JUMP_TABLE"})(xe||(xe={}));function _u(e){return e>=V.ZERO&&e<=V.NINE}function Fr(e){return e>=V.UPPER_A&&e<=V.UPPER_F||e>=V.LOWER_A&&e<=V.LOWER_F}function Sr(e){return e>=V.UPPER_A&&e<=V.UPPER_Z||e>=V.LOWER_A&&e<=V.LOWER_Z||_u(e)}function Tr(e){return e===V.EQUALS||Sr(e)}var H;(function(e){e[e.EntityStart=0]="EntityStart",e[e.NumericStart=1]="NumericStart",e[e.NumericDecimal=2]="NumericDecimal",e[e.NumericHex=3]="NumericHex",e[e.NamedEntity=4]="NamedEntity"})(H||(H={}));var be;(function(e){e[e.Legacy=0]="Legacy",e[e.Strict=1]="Strict",e[e.Attribute=2]="Attribute"})(be||(be={}));class Rr{constructor(u,t,n){this.decodeTree=u,this.emitCodePoint=t,this.errors=n,this.state=H.EntityStart,this.consumed=1,this.result=0,this.treeIndex=0,this.excess=1,this.decodeMode=be.Strict}startEntity(u){this.decodeMode=u,this.state=H.EntityStart,this.result=0,this.treeIndex=0,this.excess=1,this.consumed=1}write(u,t){switch(this.state){case H.EntityStart:return u.charCodeAt(t)===V.NUM?(this.state=H.NumericStart,this.consumed+=1,this.stateNumericStart(u,t+1)):(this.state=H.NamedEntity,this.stateNamedEntity(u,t));case H.NumericStart:return this.stateNumericStart(u,t);case H.NumericDecimal:return this.stateNumericDecimal(u,t);case H.NumericHex:return this.stateNumericHex(u,t);case H.NamedEntity:return this.stateNamedEntity(u,t)}}stateNumericStart(u,t){return t>=u.length?-1:(u.charCodeAt(t)|Dr)===V.LOWER_X?(this.state=H.NumericHex,this.consumed+=1,this.stateNumericHex(u,t+1)):(this.state=H.NumericDecimal,this.stateNumericDecimal(u,t))}addToNumericResult(u,t,n,r){if(t!==n){const o=n-t;this.result=this.result*Math.pow(r,o)+parseInt(u.substr(t,o),r),this.consumed+=o}}stateNumericHex(u,t){const n=t;for(;t>14;for(;t>14,o!==0){if(a===V.SEMI)return this.emitNamedEntityData(this.treeIndex,o,this.consumed+this.excess);this.decodeMode!==be.Strict&&(this.result=this.treeIndex,this.consumed+=this.excess,this.excess=0)}}return-1}emitNotTerminatedNamedEntity(){var u;const{result:t,decodeTree:n}=this,r=(n[t]&xe.VALUE_LENGTH)>>14;return this.emitNamedEntityData(t,r,this.consumed),(u=this.errors)===null||u===void 0||u.missingSemicolonAfterCharacterReference(),this.consumed}emitNamedEntityData(u,t,n){const{decodeTree:r}=this;return this.emitCodePoint(t===1?r[u]&~xe.VALUE_LENGTH:r[u+1],n),t===3&&this.emitCodePoint(r[u+2],n),n}end(){var u;switch(this.state){case H.NamedEntity:return this.result!==0&&(this.decodeMode!==be.Attribute||this.result===this.treeIndex)?this.emitNotTerminatedNamedEntity():0;case H.NumericDecimal:return this.emitNumericEntity(0,2);case H.NumericHex:return this.emitNumericEntity(0,3);case H.NumericStart:return(u=this.errors)===null||u===void 0||u.absenceOfDigitsInNumericCharacterReference(this.consumed),0;case H.EntityStart:return 0}}}function vt(e){let u="";const t=new Rr(e,n=>u+=wr(n));return function(r,o){let a=0,i=0;for(;(i=r.indexOf("&",i))>=0;){u+=r.slice(a,i),t.startEntity(o);const c=t.write(r,i+1);if(c<0){a=i+t.end();break}a=i+c,i=c===0?a+1:a}const s=u+r.slice(a);return u="",s}}function Mr(e,u,t,n){const r=(u&xe.BRANCH_LENGTH)>>7,o=u&xe.JUMP_TABLE;if(r===0)return o!==0&&n===o?t:-1;if(o){const s=n-o;return s<0||s>=r?-1:e[t+s]-1}let a=t,i=a+r-1;for(;a<=i;){const s=a+i>>>1,c=e[s];if(cn)i=s-1;else return e[s+r]}return-1}const yt=vt(yr);vt(Er);function Nr(e,u=be.Legacy){return yt(e,u)}function Ir(e){return yt(e,be.Strict)}function $r(e){return Object.prototype.toString.call(e)}function Su(e){return $r(e)==="[object String]"}const zr=Object.prototype.hasOwnProperty;function Lr(e,u){return zr.call(e,u)}function uu(e){return Array.prototype.slice.call(arguments,1).forEach(function(t){if(t){if(typeof t!="object")throw new TypeError(t+"must be object");Object.keys(t).forEach(function(n){e[n]=t[n]})}}),e}function Et(e,u,t){return[].concat(e.slice(0,u),t,e.slice(u+1))}function Tu(e){return!(e>=55296&&e<=57343||e>=64976&&e<=65007||(e&65535)===65535||(e&65535)===65534||e>=0&&e<=8||e===11||e>=14&&e<=31||e>=127&&e<=159||e>1114111)}function Oe(e){if(e>65535){e-=65536;const u=55296+(e>>10),t=56320+(e&1023);return String.fromCharCode(u,t)}return String.fromCharCode(e)}const Ct=/\\([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/g,Br=/&([a-z#][a-z0-9]{1,31});/gi,Or=new RegExp(Ct.source+"|"+Br.source,"gi"),Pr=/^#((?:x[a-f0-9]{1,8}|[0-9]{1,8}))$/i;function qr(e,u){if(u.charCodeAt(0)===35&&Pr.test(u)){const n=u[1].toLowerCase()==="x"?parseInt(u.slice(2),16):parseInt(u.slice(1),10);return Tu(n)?Oe(n):e}const t=Nr(e);return t!==e?t:e}function Ur(e){return e.indexOf("\\")<0?e:e.replace(Ct,"$1")}function Re(e){return e.indexOf("\\")<0&&e.indexOf("&")<0?e:e.replace(Or,function(u,t,n){return t||qr(u,n)})}const jr=/[&<>"]/,Hr=/[&<>"]/g,Vr={"&":"&","<":"<",">":">",'"':"""};function Zr(e){return Vr[e]}function _e(e){return jr.test(e)?e.replace(Hr,Zr):e}const Gr=/[.?*+^$[\]\\(){}|-]/g;function Wr(e){return e.replace(Gr,"\\$&")}function B(e){switch(e){case 9:case 32:return!0}return!1}function Pe(e){if(e>=8192&&e<=8202)return!0;switch(e){case 9:case 10:case 11:case 12:case 13:case 32:case 160:case 5760:case 8239:case 8287:case 12288:return!0}return!1}function wt(e){return Fu.test(e)||kt.test(e)}function qe(e){return wt(Oe(e))}function Ue(e){switch(e){case 33:case 34:case 35:case 36:case 37:case 38:case 39:case 40:case 41:case 42:case 43:case 44:case 45:case 46:case 47:case 58:case 59:case 60:case 61:case 62:case 63:case 64:case 91:case 92:case 93:case 94:case 95:case 96:case 123:case 124:case 125:case 126:return!0;default:return!1}}function tu(e){return e=e.trim().replace(/\s+/g," "),"ẞ".toLowerCase()==="Ṿ"&&(e=e.replace(/ẞ/g,"ß")),e.toLowerCase().toUpperCase()}function Gu(e){return e===32||e===9||e===10||e===13}function nu(e){let u=0;for(;u=u&&Gu(e.charCodeAt(t));t--);return e.slice(u,t+1)}const Kr={mdurl:kr,ucmicro:vr},Jr=Object.freeze(Object.defineProperty({__proto__:null,arrayReplaceAt:Et,asciiTrim:nu,assign:uu,escapeHtml:_e,escapeRE:Wr,fromCodePoint:Oe,has:Lr,isMdAsciiPunct:Ue,isPunctChar:wt,isPunctCharCode:qe,isSpace:B,isString:Su,isValidEntityCode:Tu,isWhiteSpace:Pe,lib:Kr,normalizeReference:tu,unescapeAll:Re,unescapeMd:Ur},Symbol.toStringTag,{value:"Module"}));function Qr(e,u,t){let n,r,o,a;const i=e.posMax,s=e.pos;for(e.pos=u+1,n=1;e.pos32))return o;if(n===41){if(a===0)break;a--}r++}return u===r||a!==0||(o.str=Re(e.slice(u,r)),o.pos=r,o.ok=!0),o}function Yr(e,u,t,n){let r,o=u;const a={ok:!1,can_continue:!1,pos:0,str:"",marker:0};if(n)a.str=n.str,a.marker=n.marker;else{if(o>=t)return a;let i=e.charCodeAt(o);if(i!==34&&i!==39&&i!==40)return a;u++,o++,i===40&&(i=41),a.marker=i}for(;o"+_e(o.content)+""};de.code_block=function(e,u,t,n,r){const o=e[u];return""+_e(e[u].content)+` +`};de.fence=function(e,u,t,n,r){const o=e[u],a=o.info?Re(o.info).trim():"";let i="",s="";if(a){const d=a.split(/(\s+)/g);i=d[0],s=d.slice(2).join("")}let c;if(t.highlight?c=t.highlight(o.content,i,s)||_e(o.content):c=_e(o.content),c.indexOf("${c} +`}return`
${c}
+`};de.image=function(e,u,t,n,r){const o=e[u];return o.attrs[o.attrIndex("alt")][1]=r.renderInlineAsText(o.children,t,n),r.renderToken(e,u,t)};de.hardbreak=function(e,u,t){return t.xhtmlOut?`
+`:`
+`};de.softbreak=function(e,u,t){return t.breaks?t.xhtmlOut?`
+`:`
+`:` +`};de.text=function(e,u){return _e(e[u].content)};de.html_block=function(e,u){return e[u].content};de.html_inline=function(e,u){return e[u].content};function Me(){this.rules=uu({},de)}Me.prototype.renderAttrs=function(u){let t,n,r;if(!u.attrs)return"";for(r="",t=0,n=u.attrs.length;t +`:">",o};Me.prototype.renderInline=function(e,u,t){let n="";const r=this.rules;for(let o=0,a=e.length;o=0&&(n=this.attrs[t][1]),n};ie.prototype.attrJoin=function(u,t){const n=this.attrIndex(u);n<0?this.attrPush([u,t]):this.attrs[n][1]=this.attrs[n][1]+" "+t};function At(e,u,t){this.src=e,this.env=t,this.tokens=[],this.inlineMode=!1,this.md=u}At.prototype.Token=ie;const u0=/\r\n?|\n/g,t0=/\0/g;function n0(e){let u;u=e.src.replace(u0,` +`),u=u.replace(t0,"�"),e.src=u}function r0(e){let u;e.inlineMode?(u=new e.Token("inline","",0),u.content=e.src,u.map=[0,1],u.children=[],e.tokens.push(u)):e.md.block.parse(e.src,e.md,e.env,e.tokens)}function o0(e){const u=e.tokens;for(let t=0,n=u.length;t\s]/i.test(e)}function a0(e){return/^<\/a\s*>/i.test(e)}function s0(e){const u=e.tokens;if(e.md.options.linkify)for(let t=0,n=u.length;t=0;a--){const i=r[a];if(i.type==="link_close"){for(a--;r[a].level!==i.level&&r[a].type!=="link_open";)a--;continue}if(i.type==="html_inline"&&(i0(i.content)&&o>0&&o--,a0(i.content)&&o++),!(o>0)&&i.type==="text"&&e.md.linkify.test(i.content)){const s=i.content;let c=e.md.linkify.match(s);const d=[];let f=i.level,p=0;c.length>0&&c[0].index===0&&a>0&&r[a-1].type==="text_special"&&(c=c.slice(1));for(let b=0;bp){const S=new e.Token("text","",0);S.content=s.slice(p,g),S.level=f,d.push(S)}const E=new e.Token("link_open","a",1);E.attrs=[["href",m]],E.level=f++,E.markup="linkify",E.info="auto",d.push(E);const A=new e.Token("text","",0);A.content=h,A.level=f,d.push(A);const T=new e.Token("link_close","a",-1);T.level=--f,T.markup="linkify",T.info="auto",d.push(T),p=c[b].lastIndex}if(p=0;t--){const n=e[t];n.type==="text"&&!u&&(n.content=n.content.replace(l0,f0)),n.type==="link_open"&&n.info==="auto"&&u--,n.type==="link_close"&&n.info==="auto"&&u++}}function b0(e){let u=0;for(let t=e.length-1;t>=0;t--){const n=e[t];n.type==="text"&&!u&&Dt.test(n.content)&&(n.content=n.content.replace(/\+-/g,"±").replace(/\.{2,}/g,"…").replace(/([?!])…/g,"$1..").replace(/([?!]){4,}/g,"$1$1$1").replace(/,{2,}/g,",").replace(/(^|[^-])---(?=[^-]|$)/mg,"$1—").replace(/(^|\s)--(?=\s|$)/mg,"$1–").replace(/(^|[^-\s])--(?=[^-\s]|$)/mg,"$1–")),n.type==="link_open"&&n.info==="auto"&&u--,n.type==="link_close"&&n.info==="auto"&&u++}}function h0(e){let u;if(e.md.options.typographer)for(u=e.tokens.length-1;u>=0;u--)e.tokens[u].type==="inline"&&(c0.test(e.tokens[u].content)&&p0(e.tokens[u].children),Dt.test(e.tokens[u].content)&&b0(e.tokens[u].children))}const m0=/['"]/,Wu=/['"]/g,Ku="’";function Ke(e,u,t,n){e[u]||(e[u]=[]),e[u].push({pos:t,ch:n})}function g0(e,u){let t="",n=0;u.sort((r,o)=>r.pos-o.pos);for(let r=0;r=0&&!(n[t].level<=i);t--);if(n.length=t+1,a.type!=="text")continue;const s=a.content;let c=0;const d=s.length;e:for(;c=0)m=s.charCodeAt(f.index-1);else for(t=o-1;t>=0&&!(e[t].type==="softbreak"||e[t].type==="hardbreak");t--)if(e[t].content){m=e[t].content.charCodeAt(e[t].content.length-1);break}let h=32;if(c=48&&m<=57&&(b=p=!1),p&&b&&(p=g,b=E),!p&&!b){l&&Ke(r,o,f.index,Ku);continue}if(b)for(t=n.length-1;t>=0;t--){let S=n[t];if(n[t].level=0;u--)e.tokens[u].type!=="inline"||!m0.test(e.tokens[u].content)||x0(e.tokens[u].children,e)}function _0(e){let u,t;const n=e.tokens,r=n.length;for(let o=0;o0&&this.level++,this.tokens.push(n),n};fe.prototype.isEmpty=function(u){return this.bMarks[u]+this.tShift[u]>=this.eMarks[u]};fe.prototype.skipEmptyLines=function(u){for(let t=this.lineMax;ut;)if(!B(this.src.charCodeAt(--u)))return u+1;return u};fe.prototype.skipChars=function(u,t){for(let n=this.src.length;un;)if(t!==this.src.charCodeAt(--u))return u+1;return u};fe.prototype.getLines=function(u,t,n,r){if(u>=t)return"";const o=new Array(t-u);for(let a=0,i=u;in?o[a]=new Array(s-n+1).join(" ")+this.src.slice(d,f):o[a]=this.src.slice(d,f)}return o.join("")};fe.prototype.Token=ie;const v0=65536;function pu(e,u){const t=e.bMarks[u]+e.tShift[u],n=e.eMarks[u];return e.src.slice(t,n)}function Ju(e){const u=[],t=e.length;let n=0,r=e.charCodeAt(n),o=!1,a=0,i="";for(;nt)return!1;let r=u+1;if(e.sCount[r]=4)return!1;let o=e.bMarks[r]+e.tShift[r];if(o>=e.eMarks[r])return!1;const a=e.src.charCodeAt(o++);if(a!==124&&a!==45&&a!==58||o>=e.eMarks[r])return!1;const i=e.src.charCodeAt(o++);if(i!==124&&i!==45&&i!==58&&!B(i)||a===45&&B(i))return!1;for(;o=4)return!1;c=Ju(s),c.length&&c[0]===""&&c.shift(),c.length&&c[c.length-1]===""&&c.pop();const f=c.length;if(f===0||f!==d.length)return!1;if(n)return!0;const p=e.parentType;e.parentType="table";const b=e.md.block.ruler.getRules("blockquote"),l=e.push("table_open","table",1),m=[u,0];l.map=m;const h=e.push("thead_open","thead",1);h.map=[u,u+1];const g=e.push("tr_open","tr",1);g.map=[u,u+1];for(let T=0;T=4||(c=Ju(s),c.length&&c[0]===""&&c.shift(),c.length&&c[c.length-1]===""&&c.pop(),A+=f-c.length,A>v0))break;if(r===u+2){const M=e.push("tbody_open","tbody",1);M.map=E=[u+2,0]}const S=e.push("tr_open","tr",1);S.map=[r,r+1];for(let M=0;M=4){n++,r=n;continue}break}e.line=r;const o=e.push("code_block","code",0);return o.content=e.getLines(u,r,4+e.blkIndent,!1)+` +`,o.map=[u,e.line],!0}function C0(e,u,t,n){let r=e.bMarks[u]+e.tShift[u],o=e.eMarks[u];if(e.sCount[u]-e.blkIndent>=4||r+3>o)return!1;const a=e.src.charCodeAt(r);if(a!==126&&a!==96)return!1;let i=r;r=e.skipChars(r,a);let s=r-i;if(s<3)return!1;const c=e.src.slice(i,r),d=e.src.slice(r,o);if(a===96&&d.indexOf(String.fromCharCode(a))>=0)return!1;if(n)return!0;let f=u,p=!1;for(;f++,!(f>=t||(r=i=e.bMarks[f]+e.tShift[f],o=e.eMarks[f],r=4)&&(r=e.skipChars(r,a),!(r-i=4||e.src.charCodeAt(r)!==62)return!1;if(n)return!0;const i=[],s=[],c=[],d=[],f=e.md.block.ruler.getRules("blockquote"),p=e.parentType;e.parentType="blockquote";let b=!1,l;for(l=u;l=o)break;if(e.src.charCodeAt(r++)===62&&!A){let S=e.sCount[l]+1,M,L;e.src.charCodeAt(r)===32?(r++,S++,L=!1,M=!0):e.src.charCodeAt(r)===9?(M=!0,(e.bsCount[l]+S)%4===3?(r++,S++,L=!1):L=!0):M=!1;let j=S;for(i.push(e.bMarks[l]),e.bMarks[l]=r;r=o,s.push(e.bsCount[l]),e.bsCount[l]=e.sCount[l]+1+(M?1:0),c.push(e.sCount[l]),e.sCount[l]=j-S,d.push(e.tShift[l]),e.tShift[l]=r-e.bMarks[l];continue}if(b)break;let T=!1;for(let S=0,M=f.length;S";const g=[u,0];h.map=g,e.md.block.tokenize(e,u,l);const E=e.push("blockquote_close","blockquote",-1);E.markup=">",e.lineMax=a,e.parentType=p,g[1]=e.line;for(let A=0;A=4)return!1;let o=e.bMarks[u]+e.tShift[u];const a=e.src.charCodeAt(o++);if(a!==42&&a!==45&&a!==95)return!1;let i=1;for(;o=n)return-1;let o=e.src.charCodeAt(r++);if(o<48||o>57)return-1;for(;;){if(r>=n)return-1;if(o=e.src.charCodeAt(r++),o>=48&&o<=57){if(r-t>=10)return-1;continue}if(o===41||o===46)break;return-1}return r=4||e.listIndent>=0&&e.sCount[s]-e.listIndent>=4&&e.sCount[s]=e.blkIndent&&(d=!0);let f,p,b;if((b=Xu(e,s))>=0){if(f=!0,a=e.bMarks[s]+e.tShift[s],p=Number(e.src.slice(a,b-1)),d&&p!==1)return!1}else if((b=Qu(e,s))>=0)f=!1;else return!1;if(d&&e.skipSpaces(b)>=e.eMarks[s])return!1;if(n)return!0;const l=e.src.charCodeAt(b-1),m=e.tokens.length;f?(i=e.push("ordered_list_open","ol",1),p!==1&&(i.attrs=[["start",p]])):i=e.push("bullet_list_open","ul",1);const h=[s,0];i.map=h,i.markup=String.fromCharCode(l);let g=!1;const E=e.md.block.ruler.getRules("list"),A=e.parentType;for(e.parentType="list";s=r?L=1:L=S-T,L>4&&(L=1);const j=T+L;i=e.push("list_item_open","li",1),i.markup=String.fromCharCode(l);const Q=[s,0];i.map=Q,f&&(i.info=e.src.slice(a,b-1));const ue=e.tight,ve=e.tShift[s],Ne=e.sCount[s],he=e.listIndent;if(e.listIndent=e.blkIndent,e.blkIndent=j,e.tight=!0,e.tShift[s]=M-e.bMarks[s],e.sCount[s]=S,M>=r&&e.isEmpty(s+1)?e.line=Math.min(e.line+2,t):e.md.block.tokenize(e,s,t,!0),(!e.tight||g)&&(c=!1),g=e.line-s>1&&e.isEmpty(e.line-1),e.blkIndent=e.listIndent,e.listIndent=he,e.tShift[s]=ve,e.sCount[s]=Ne,e.tight=ue,i=e.push("list_item_close","li",-1),i.markup=String.fromCharCode(l),s=e.line,Q[1]=s,s>=t||e.sCount[s]=4)break;let ye=!1;for(let te=0,we=E.length;te=4||e.src.charCodeAt(r)!==91)return!1;function i(E){const A=e.lineMax;if(E>=A||e.isEmpty(E))return null;let T=!1;if(e.sCount[E]-e.blkIndent>3&&(T=!0),e.sCount[E]<0&&(T=!0),!T){const L=e.md.block.ruler.getRules("reference"),j=e.parentType;e.parentType="reference";let Q=!1;for(let ue=0,ve=L.length;ue"u"&&(e.env.references={}),typeof e.env.references[g]>"u"&&(e.env.references[g]={title:h,href:f}),e.line=a),!0):!1}const T0=["address","article","aside","base","basefont","blockquote","body","caption","center","col","colgroup","dd","details","dialog","dir","div","dl","dt","fieldset","figcaption","figure","footer","form","frame","frameset","h1","h2","h3","h4","h5","h6","head","header","hr","html","iframe","legend","li","link","main","menu","menuitem","nav","noframes","ol","optgroup","option","p","param","search","section","summary","table","tbody","td","tfoot","th","thead","title","tr","track","ul"],R0="[a-zA-Z_:][a-zA-Z0-9:._-]*",M0="[^\"'=<>`\\x00-\\x20]+",N0="'[^']*'",I0='"[^"]*"',$0="(?:"+M0+"|"+N0+"|"+I0+")",z0="(?:\\s+"+R0+"(?:\\s*=\\s*"+$0+")?)",Ft="<[A-Za-z][A-Za-z0-9\\-]*"+z0+"*\\s*\\/?>",St="<\\/[A-Za-z][A-Za-z0-9\\-]*\\s*>",L0="",B0="<[?][\\s\\S]*?[?]>",O0="]*>",P0="",q0=new RegExp("^(?:"+Ft+"|"+St+"|"+L0+"|"+B0+"|"+O0+"|"+P0+")"),U0=new RegExp("^(?:"+Ft+"|"+St+")"),Ee=[[/^<(script|pre|style|textarea)(?=(\s|>|$))/i,/<\/(script|pre|style|textarea)>/i,!0],[/^/,!0],[/^<\?/,/\?>/,!0],[/^/,!0],[/^/,!0],[new RegExp("^|$))","i"),/^$/,!0],[new RegExp(U0.source+"\\s*$"),/^$/,!1]];function j0(e,u,t,n){let r=e.bMarks[u]+e.tShift[u],o=e.eMarks[u];if(e.sCount[u]-e.blkIndent>=4||!e.md.options.html||e.src.charCodeAt(r)!==60)return!1;let a=e.src.slice(r,o),i=0;for(;i=4)return!1;let a=e.src.charCodeAt(r);if(a!==35||r>=o)return!1;let i=1;for(a=e.src.charCodeAt(++r);a===35&&r6||rr&&B(e.src.charCodeAt(s-1))&&(o=s),e.line=u+1;const c=e.push("heading_open","h"+String(i),1);c.markup="########".slice(0,i),c.map=[u,e.line];const d=e.push("inline","",0);d.content=nu(e.src.slice(r,o)),d.map=[u,e.line],d.children=[];const f=e.push("heading_close","h"+String(i),-1);return f.markup="########".slice(0,i),!0}function V0(e,u,t){const n=e.md.block.ruler.getRules("paragraph");if(e.sCount[u]-e.blkIndent>=4)return!1;const r=e.parentType;e.parentType="paragraph";let o=0,a,i=u+1;for(;i3)continue;if(e.sCount[i]>=e.blkIndent){let b=e.bMarks[i]+e.tShift[i];const l=e.eMarks[i];if(b=l))){o=a===61?1:2;break}}if(e.sCount[i]<0)continue;let p=!1;for(let b=0,l=n.length;b3||e.sCount[o]<0)continue;let c=!1;for(let d=0,f=n.length;d=t||e.sCount[a]=o){e.line=t;break}const s=e.line;let c=!1;for(let d=0;d=e.line)throw new Error("block rule didn't increment state.line");break}if(!c)throw new Error("none of the block rules matched");e.tight=!i,e.isEmpty(e.line-1)&&(i=!0),a=e.line,a0&&(this.level++,this._prev_delimiters.push(this.delimiters),this.delimiters=[],r={delimiters:this.delimiters}),this.pendingLevel=this.level,this.tokens.push(n),this.tokens_meta.push(r),n};Ze.prototype.scanDelims=function(e,u){const t=this.posMax,n=this.src.charCodeAt(e);let r;if(e===0)r=32;else if(e===1)r=this.src.charCodeAt(0),(r&63488)===55296&&(r=65533);else if(r=this.src.charCodeAt(e-1),(r&64512)===56320){const h=this.src.charCodeAt(e-2);r=(h&64512)===55296?65536+(h-55296<<10)+(r-56320):65533}else(r&64512)===55296&&(r=65533);let o=e;for(;o0)return!1;const t=e.pos,n=e.posMax;if(t+3>n||e.src.charCodeAt(t)!==58||e.src.charCodeAt(t+1)!==47||e.src.charCodeAt(t+2)!==47)return!1;const r=e.pending.match(K0);if(!r)return!1;const o=r[1],a=e.md.linkify.matchAtStart(e.src.slice(t-o.length));if(!a)return!1;let i=a.url;if(i.length<=o.length)return!1;let s=i.length;for(;s>0&&i.charCodeAt(s-1)===42;)s--;s!==i.length&&(i=i.slice(0,s));const c=e.md.normalizeLink(i);if(!e.md.validateLink(c))return!1;if(!u){e.pending=e.pending.slice(0,-o.length);const d=e.push("link_open","a",1);d.attrs=[["href",c]],d.markup="linkify",d.info="auto";const f=e.push("text","",0);f.content=e.md.normalizeLinkText(i);const p=e.push("link_close","a",-1);p.markup="linkify",p.info="auto"}return e.pos+=i.length-o.length,!0}function Q0(e,u){let t=e.pos;if(e.src.charCodeAt(t)!==10)return!1;const n=e.pending.length-1,r=e.posMax;if(!u)if(n>=0&&e.pending.charCodeAt(n)===32)if(n>=1&&e.pending.charCodeAt(n-1)===32){let o=n-1;for(;o>=1&&e.pending.charCodeAt(o-1)===32;)o--;e.pending=e.pending.slice(0,o),e.push("hardbreak","br",0)}else e.pending=e.pending.slice(0,-1),e.push("softbreak","br",0);else e.push("softbreak","br",0);for(t++;t?@[]^_`{|}~-".split("").forEach(function(e){Mu[e.charCodeAt(0)]=1});function X0(e,u){let t=e.pos;const n=e.posMax;if(e.src.charCodeAt(t)!==92||(t++,t>=n))return!1;let r=e.src.charCodeAt(t);if(r===10){for(u||e.push("hardbreak","br",0),t++;t=55296&&r<=56319&&t+1=56320&&i<=57343&&(o+=e.src[t+1],t++)}const a="\\"+o;if(!u){const i=e.push("text_special","",0);r<256&&Mu[r]!==0?i.content=o:i.content=a,i.markup=a,i.info="escape"}return e.pos=t+1,!0}function Y0(e,u){let t=e.pos;if(e.src.charCodeAt(t)!==96)return!1;const r=t;t++;const o=e.posMax;for(;t=0;n--){const r=u[n];if(r.marker!==95&&r.marker!==42||r.end===-1)continue;const o=u[r.end],a=n>0&&u[n-1].end===r.end+1&&u[n-1].marker===r.marker&&u[n-1].token===r.token-1&&u[r.end+1].token===o.token+1,i=String.fromCharCode(r.marker),s=e.tokens[r.token];s.type=a?"strong_open":"em_open",s.tag=a?"strong":"em",s.nesting=1,s.markup=a?i+i:i,s.content="";const c=e.tokens[o.token];c.type=a?"strong_close":"em_close",c.tag=a?"strong":"em",c.nesting=-1,c.markup=a?i+i:i,c.content="",a&&(e.tokens[u[n-1].token].content="",e.tokens[u[r.end+1].token].content="",n--)}}function no(e){const u=e.tokens_meta,t=e.tokens_meta.length;et(e,e.delimiters);for(let n=0;n=f)return!1;if(s=l,r=e.md.helpers.parseLinkDestination(e.src,l,e.posMax),r.ok){for(a=e.md.normalizeLink(r.str),e.md.validateLink(a)?l=r.pos:a="",s=l;l=f||e.src.charCodeAt(l)!==41)&&(c=!0),l++}if(c){if(typeof e.env.references>"u")return!1;if(l=0?n=e.src.slice(s,l++):l=b+1):l=b+1,n||(n=e.src.slice(p,b)),o=e.env.references[tu(n)],!o)return e.pos=d,!1;a=o.href,i=o.title}if(!u){e.pos=p,e.posMax=b;const m=e.push("link_open","a",1),h=[["href",a]];m.attrs=h,i&&h.push(["title",i]),e.linkLevel++,e.md.inline.tokenize(e),e.linkLevel--,e.push("link_close","a",-1)}return e.pos=l,e.posMax=f,!0}function oo(e,u){let t,n,r,o,a,i,s,c,d="";const f=e.pos,p=e.posMax;if(e.src.charCodeAt(e.pos)!==33||e.src.charCodeAt(e.pos+1)!==91)return!1;const b=e.pos+2,l=e.md.helpers.parseLinkLabel(e,e.pos+1,!1);if(l<0)return!1;if(o=l+1,o=p)return!1;for(c=o,i=e.md.helpers.parseLinkDestination(e.src,o,e.posMax),i.ok&&(d=e.md.normalizeLink(i.str),e.md.validateLink(d)?o=i.pos:d=""),c=o;o=p||e.src.charCodeAt(o)!==41)return e.pos=f,!1;o++}else{if(typeof e.env.references>"u")return!1;if(o=0?r=e.src.slice(c,o++):o=l+1):o=l+1,r||(r=e.src.slice(b,l)),a=e.env.references[tu(r)],!a)return e.pos=f,!1;d=a.href,s=a.title}if(!u){n=e.src.slice(b,l);const m=[];e.md.inline.parse(n,e.md,e.env,m);const h=e.push("image","img",0),g=[["src",d],["alt",""]];h.attrs=g,h.children=m,h.content=n,s&&g.push(["title",s])}return e.pos=o,e.posMax=p,!0}const io=/^([a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)$/,ao=/^([a-zA-Z][a-zA-Z0-9+.-]{1,31}):([^<>\x00-\x20]*)$/;function so(e,u){let t=e.pos;if(e.src.charCodeAt(t)!==60)return!1;const n=e.pos,r=e.posMax;for(;;){if(++t>=r)return!1;const a=e.src.charCodeAt(t);if(a===60)return!1;if(a===62)break}const o=e.src.slice(n+1,t);if(ao.test(o)){const a=e.md.normalizeLink(o);if(!e.md.validateLink(a))return!1;if(!u){const i=e.push("link_open","a",1);i.attrs=[["href",a]],i.markup="autolink",i.info="auto";const s=e.push("text","",0);s.content=e.md.normalizeLinkText(o);const c=e.push("link_close","a",-1);c.markup="autolink",c.info="auto"}return e.pos+=o.length+2,!0}if(io.test(o)){const a=e.md.normalizeLink("mailto:"+o);if(!e.md.validateLink(a))return!1;if(!u){const i=e.push("link_open","a",1);i.attrs=[["href",a]],i.markup="autolink",i.info="auto";const s=e.push("text","",0);s.content=e.md.normalizeLinkText(o);const c=e.push("link_close","a",-1);c.markup="autolink",c.info="auto"}return e.pos+=o.length+2,!0}return!1}function co(e){return/^\s]/i.test(e)}function lo(e){return/^<\/a\s*>/i.test(e)}function fo(e){const u=e|32;return u>=97&&u<=122}function po(e,u){if(!e.md.options.html)return!1;const t=e.posMax,n=e.pos;if(e.src.charCodeAt(n)!==60||n+2>=t)return!1;const r=e.src.charCodeAt(n+1);if(r!==33&&r!==63&&r!==47&&!fo(r))return!1;const o=e.src.slice(n).match(q0);if(!o)return!1;if(!u){const a=e.push("html_inline","",0);a.content=o[0],co(a.content)&&e.linkLevel++,lo(a.content)&&e.linkLevel--}return e.pos+=o[0].length,!0}const bo=/^&#((?:x[a-f0-9]{1,6}|[0-9]{1,7}));/i,ho=/^&([a-z][a-z0-9]{1,31});/i;function mo(e,u){const t=e.pos,n=e.posMax;if(e.src.charCodeAt(t)!==38||t+1>=n)return!1;if(e.src.charCodeAt(t+1)===35){const o=e.src.slice(t).match(bo);if(o){if(!u){const a=o[1][0].toLowerCase()==="x"?parseInt(o[1].slice(1),16):parseInt(o[1],10),i=e.push("text_special","",0);i.content=Tu(a)?Oe(a):Oe(65533),i.markup=o[0],i.info="entity"}return e.pos+=o[0].length,!0}}else{const o=e.src.slice(t).match(ho);if(o){const a=Ir(o[0]);if(a!==o[0]){if(!u){const i=e.push("text_special","",0);i.content=a,i.markup=o[0],i.info="entity"}return e.pos+=o[0].length,!0}}}return!1}function ut(e){const u={},t=e.length;if(!t)return;let n=0,r=-2;const o=[];for(let a=0;as;c-=o[c]+1){const f=e[c];if(f.marker===i.marker&&f.open&&f.end<0){let p=!1;if((f.close||i.open)&&(f.length+i.length)%3===0&&(f.length%3!==0||i.length%3!==0)&&(p=!0),!p){const b=c>0&&!e[c-1].open?o[c-1]+1:0;o[a]=a-c+b,o[c]=b,i.open=!1,f.end=a,f.close=!1,d=-1,r=-2;break}}}d!==-1&&(u[i.marker][(i.open?3:0)+(i.length||0)%3]=d)}}function go(e){const u=e.tokens_meta,t=e.tokens_meta.length;ut(e.delimiters);for(let n=0;n0&&n++,r[u].type==="text"&&u+1=e.pos)throw new Error("inline rule didn't increment state.pos");break}}else e.pos=e.posMax;a||e.pos++,o[u]=e.pos};Ge.prototype.tokenize=function(e){const u=this.ruler.getRules(""),t=u.length,n=e.posMax,r=e.md.options.maxNesting;for(;e.pos=e.pos)throw new Error("inline rule didn't increment state.pos");break}}if(a){if(e.pos>=n)break;continue}e.pending+=e.src[e.pos++]}e.pending&&e.pushPending()};Ge.prototype.parse=function(e,u,t,n){const r=new this.State(e,u,t,n);this.tokenize(r);const o=this.ruler2.getRules(""),a=o.length;for(let i=0;i|$))",u.tpl_email_fuzzy="(^|"+t+'|"|\\(|'+u.src_ZCc+")("+u.src_email_name+"@"+u.tpl_host_fuzzy_strict+")",u.tpl_link_fuzzy="(^|(?![.:/\\-_@])(?:[$+<=>^`||]|"+u.src_ZPCc+"))((?![$+<=>^`||])"+u.tpl_host_port_fuzzy_strict+u.src_path+")",u.tpl_link_no_ip_fuzzy="(^|(?![.:/\\-_@])(?:[$+<=>^`||]|"+u.src_ZPCc+"))((?![$+<=>^`||])"+u.tpl_host_port_no_ip_fuzzy_strict+u.src_path+")",u}function vu(e){return Array.prototype.slice.call(arguments,1).forEach(function(t){t&&Object.keys(t).forEach(function(n){e[n]=t[n]})}),e}function ou(e){return Object.prototype.toString.call(e)}function _o(e){return ou(e)==="[object String]"}function vo(e){return ou(e)==="[object Object]"}function yo(e){return ou(e)==="[object RegExp]"}function tt(e){return ou(e)==="[object Function]"}function Eo(e){return e.replace(/[.?*+^$[\]\\(){}|-]/g,"\\$&")}const Mt={fuzzyLink:!0,fuzzyEmail:!0,fuzzyIP:!1};function Co(e){return Object.keys(e||{}).reduce(function(u,t){return u||Mt.hasOwnProperty(t)},!1)}const wo={"http:":{validate:function(e,u,t){const n=e.slice(u);return t.re.http||(t.re.http=new RegExp("^\\/\\/"+t.re.src_auth+t.re.src_host_port_strict+t.re.src_path,"i")),t.re.http.test(n)?n.match(t.re.http)[0].length:0}},"https:":"http:","ftp:":"http:","//":{validate:function(e,u,t){const n=e.slice(u);return t.re.no_http||(t.re.no_http=new RegExp("^"+t.re.src_auth+"(?:localhost|(?:(?:"+t.re.src_domain+")\\.)+"+t.re.src_domain_root+")"+t.re.src_port+t.re.src_host_terminator+t.re.src_path,"i")),t.re.no_http.test(n)?u>=3&&e[u-3]===":"||u>=3&&e[u-3]==="/"?0:n.match(t.re.no_http)[0].length:0}},"mailto:":{validate:function(e,u,t){const n=e.slice(u);return t.re.mailto||(t.re.mailto=new RegExp("^"+t.re.src_email_name+"@"+t.re.src_host_strict,"i")),t.re.mailto.test(n)?n.match(t.re.mailto)[0].length:0}}},Ao="a[cdefgilmnoqrstuwxz]|b[abdefghijmnorstvwyz]|c[acdfghiklmnoruvwxyz]|d[ejkmoz]|e[cegrstu]|f[ijkmor]|g[abdefghilmnpqrstuwy]|h[kmnrtu]|i[delmnoqrst]|j[emop]|k[eghimnprwyz]|l[abcikrstuvy]|m[acdeghklmnopqrstuvwxyz]|n[acefgilopruz]|om|p[aefghklmnrstwy]|qa|r[eosuw]|s[abcdeghijklmnortuvxyz]|t[cdfghjklmnortvwz]|u[agksyz]|v[aceginu]|w[fs]|y[et]|z[amw]",Do="biz|com|edu|gov|net|org|pro|web|xxx|aero|asia|coop|info|museum|name|shop|рф".split("|");function Fo(e){return function(u,t){const n=u.slice(t);return e.test(n)?n.match(e)[0].length:0}}function nt(){return function(e,u){u.normalize(e)}}function Xe(e){const u=e.re=ko(e.__opts__),t=e.__tlds__.slice();e.onCompile(),e.__tlds_replaced__||t.push(Ao),t.push(u.src_xn),u.src_tlds=t.join("|");function n(i){return i.replace("%TLDS%",u.src_tlds)}u.email_fuzzy=RegExp(n(u.tpl_email_fuzzy),"i"),u.email_fuzzy_global=RegExp(n(u.tpl_email_fuzzy),"ig"),u.link_fuzzy=RegExp(n(u.tpl_link_fuzzy),"i"),u.link_fuzzy_global=RegExp(n(u.tpl_link_fuzzy),"ig"),u.link_no_ip_fuzzy=RegExp(n(u.tpl_link_no_ip_fuzzy),"i"),u.link_no_ip_fuzzy_global=RegExp(n(u.tpl_link_no_ip_fuzzy),"ig"),u.host_fuzzy_test=RegExp(n(u.tpl_host_fuzzy_test),"i");const r=[];e.__compiled__={};function o(i,s){throw new Error('(LinkifyIt) Invalid schema "'+i+'": '+s)}Object.keys(e.__schemas__).forEach(function(i){const s=e.__schemas__[i];if(s===null)return;const c={validate:null,link:null};if(e.__compiled__[i]=c,vo(s)){yo(s.validate)?c.validate=Fo(s.validate):tt(s.validate)?c.validate=s.validate:o(i,s),tt(s.normalize)?c.normalize=s.normalize:s.normalize?o(i,s):c.normalize=nt();return}if(_o(s)){r.push(i);return}o(i,s)}),r.forEach(function(i){e.__compiled__[e.__schemas__[i]]&&(e.__compiled__[i].validate=e.__compiled__[e.__schemas__[i]].validate,e.__compiled__[i].normalize=e.__compiled__[e.__schemas__[i]].normalize)}),e.__compiled__[""]={validate:null,normalize:nt()};const a=Object.keys(e.__compiled__).filter(function(i){return i.length>0&&e.__compiled__[i]}).map(Eo).join("|");e.re.schema_test=RegExp("(^|(?!_)(?:[><|]|"+u.src_ZPCc+"))("+a+")","i"),e.re.schema_search=RegExp("(^|(?!_)(?:[><|]|"+u.src_ZPCc+"))("+a+")","ig"),e.re.schema_at_start=RegExp("^"+e.re.schema_search.source,"i"),e.re.pretest=RegExp("("+e.re.schema_test.source+")|("+e.re.host_fuzzy_test.source+")|@","i")}function Nt(e,u,t,n){const r=e.slice(t,n);this.schema=u.toLowerCase(),this.index=t,this.lastIndex=n,this.raw=r,this.text=r,this.url=r}function ee(e,u){if(!(this instanceof ee))return new ee(e,u);u||Co(e)&&(u=e,e={}),this.__opts__=vu({},Mt,u),this.__schemas__=vu({},wo,e),this.__compiled__={},this.__tlds__=Do,this.__tlds_replaced__=!1,this.re={},Xe(this)}ee.prototype.add=function(u,t){return this.__schemas__[u]=t,Xe(this),this};ee.prototype.set=function(u){return this.__opts__=vu(this.__opts__,u),this};ee.prototype.test=function(u){if(!u.length)return!1;let t,n;if(this.re.schema_test.test(u)){for(n=this.re.schema_search,n.lastIndex=0;(t=n.exec(u))!==null;)if(this.testSchemaAt(u,t[2],n.lastIndex))return!0}return!!(this.__opts__.fuzzyLink&&this.__compiled__["http:"]&&u.search(this.re.host_fuzzy_test)>=0&&u.match(this.__opts__.fuzzyIP?this.re.link_fuzzy:this.re.link_no_ip_fuzzy)!==null||this.__opts__.fuzzyEmail&&this.__compiled__["mailto:"]&&u.indexOf("@")>=0&&u.match(this.re.email_fuzzy)!==null)};ee.prototype.pretest=function(u){return this.re.pretest.test(u)};ee.prototype.testSchemaAt=function(u,t,n){return this.__compiled__[t.toLowerCase()]?this.__compiled__[t.toLowerCase()].validate(u,n,this):0};ee.prototype.match=function(u){const t=[],n=[],r=[],o=[];let a,i,s;function c(p,b){return p?b?p.index!==b.index?p.index=b.lastIndex?p:b:p:b}if(!u.length)return null;if(this.re.schema_test.test(u))for(s=this.re.schema_search,s.lastIndex=0;(a=s.exec(u))!==null;)i=this.testSchemaAt(u,a[2],s.lastIndex),i&&n.push({schema:a[2],index:a.index+a[1].length,lastIndex:a.index+a[0].length+i});if(this.__opts__.fuzzyLink&&this.__compiled__["http:"])for(s=this.__opts__.fuzzyIP?this.re.link_fuzzy_global:this.re.link_no_ip_fuzzy_global,s.lastIndex=0;(a=s.exec(u))!==null;)r.push({schema:"",index:a.index+a[1].length,lastIndex:a.index+a[0].length});if(this.__opts__.fuzzyEmail&&this.__compiled__["mailto:"])for(s=this.re.email_fuzzy_global,s.lastIndex=0;(a=s.exec(u))!==null;)o.push({schema:"mailto:",index:a.index+a[1].length,lastIndex:a.index+a[0].length});const d=[0,0,0];let f=0;for(;;){const p=[n[d[0]],o[d[1]],r[d[2]]],b=c(c(p[0],p[1]),p[2]);if(!b)break;if(b===p[0]?d[0]++:b===p[1]?d[1]++:d[2]++,b.index= 0x80 (not a basic code point)","invalid-input":"Invalid input"},mu=ce-Nu,le=Math.floor,gu=String.fromCharCode;function ge(e){throw new RangeError(Io[e])}function $o(e,u){const t=[];let n=e.length;for(;n--;)t[n]=u(e[n]);return t}function Lt(e,u){const t=e.split("@");let n="";t.length>1&&(n=t[0]+"@",e=t[1]),e=e.replace(No,".");const r=e.split("."),o=$o(r,u).join(".");return n+o}function Bt(e){const u=[];let t=0;const n=e.length;for(;t=55296&&r<=56319&&tString.fromCodePoint(...e),Lo=function(e){return e>=48&&e<58?26+(e-48):e>=65&&e<91?e-65:e>=97&&e<123?e-97:ce},rt=function(e,u){return e+22+75*(e<26)-((u!=0)<<5)},Ot=function(e,u,t){let n=0;for(e=t?le(e/To):e>>1,e+=le(e/u);e>mu*je>>1;n+=ce)e=le(e/mu);return le(n+(mu+1)*e/(e+So))},Pt=function(e){const u=[],t=e.length;let n=0,r=$t,o=It,a=e.lastIndexOf(zt);a<0&&(a=0);for(let i=0;i=128&&ge("not-basic"),u.push(e.charCodeAt(i));for(let i=a>0?a+1:0;i=t&&ge("invalid-input");const p=Lo(e.charCodeAt(i++));p>=ce&&ge("invalid-input"),p>le((Fe-n)/d)&&ge("overflow"),n+=p*d;const b=f<=o?Nu:f>=o+je?je:f-o;if(ple(Fe/l)&&ge("overflow"),d*=l}const c=u.length+1;o=Ot(n-s,c,s==0),le(n/c)>Fe-r&&ge("overflow"),r+=le(n/c),n%=c,u.splice(n++,0,r)}return String.fromCodePoint(...u)},qt=function(e){const u=[];e=Bt(e);const t=e.length;let n=$t,r=0,o=It;for(const s of e)s<128&&u.push(gu(s));const a=u.length;let i=a;for(a&&u.push(zt);i=n&&dle((Fe-r)/c)&&ge("overflow"),r+=(s-n)*c,n=s;for(const d of e)if(dFe&&ge("overflow"),d===n){let f=r;for(let p=ce;;p+=ce){const b=p<=o?Nu:p>=o+je?je:p-o;if(f=0))try{u.hostname=Ut.toASCII(u.hostname)}catch{}return Ve(Au(u))}function Wo(e){const u=Du(e,!0);if(u.hostname&&(!u.protocol||jt.indexOf(u.protocol)>=0))try{u.hostname=Ut.toUnicode(u.hostname)}catch{}return Te(Au(u),Te.defaultChars+"%")}function re(e,u){if(!(this instanceof re))return new re(e,u);u||Su(e)||(u=e||{},e="default"),this.inline=new Ge,this.block=new ru,this.core=new Ru,this.renderer=new Me,this.linkify=new ee,this.validateLink=Zo,this.normalizeLink=Go,this.normalizeLinkText=Wo,this.utils=Jr,this.helpers=uu({},e0),this.options={},this.configure(e),u&&this.set(u)}re.prototype.set=function(e){return uu(this.options,e),this};re.prototype.configure=function(e){const u=this;if(Su(e)){const t=e;if(e=jo[t],!e)throw new Error('Wrong `markdown-it` preset "'+t+'", check name')}if(!e)throw new Error("Wrong `markdown-it` preset, can't be empty");return e.options&&u.set(e.options),e.components&&Object.keys(e.components).forEach(function(t){e.components[t].rules&&u[t].ruler.enableOnly(e.components[t].rules),e.components[t].rules2&&u[t].ruler2.enableOnly(e.components[t].rules2)}),this};re.prototype.enable=function(e,u){let t=[];Array.isArray(e)||(e=[e]),["core","block","inline"].forEach(function(r){t=t.concat(this[r].ruler.enable(e,!0))},this),t=t.concat(this.inline.ruler2.enable(e,!0));const n=e.filter(function(r){return t.indexOf(r)<0});if(n.length&&!u)throw new Error("MarkdownIt. Failed to enable unknown rule(s): "+n);return this};re.prototype.disable=function(e,u){let t=[];Array.isArray(e)||(e=[e]),["core","block","inline"].forEach(function(r){t=t.concat(this[r].ruler.disable(e,!0))},this),t=t.concat(this.inline.ruler2.disable(e,!0));const n=e.filter(function(r){return t.indexOf(r)<0});if(n.length&&!u)throw new Error("MarkdownIt. Failed to disable unknown rule(s): "+n);return this};re.prototype.use=function(e){const u=[this].concat(Array.prototype.slice.call(arguments,1));return e.apply(e,u),this};re.prototype.parse=function(e,u){if(typeof e!="string")throw new Error("Input data should be a String");const t=new this.core.State(e,this,u);return this.core.process(t),t.tokens};re.prototype.render=function(e,u){return u=u||{},this.renderer.render(this.parse(e,u),this.options,u)};re.prototype.parseInline=function(e,u){const t=new this.core.State(e,this,u);return t.inlineMode=!0,this.core.process(t),t.tokens};re.prototype.renderInline=function(e,u){return u=u||{},this.renderer.render(this.parseInline(e,u),this.options,u)};const Ye="[A-Za-z$_][0-9A-Za-z$_]*",Ht=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends","using"],Vt=["true","false","null","undefined","NaN","Infinity"],Zt=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],Gt=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],Wt=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],Kt=["arguments","this","super","console","window","document","localStorage","sessionStorage","module","global"],Jt=[].concat(Wt,Zt,Gt);function Ko(e){const u=e.regex,t=(W,{after:ae})=>{const pe="",end:""},o=/<[A-Za-z0-9\\._:-]+\s*\/>/,a={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(W,ae)=>{const pe=W[0].length+W.index,Ae=W.input[pe];if(Ae==="<"||Ae===","){ae.ignoreMatch();return}Ae===">"&&(t(W,{after:pe})||ae.ignoreMatch());let Ie;const $e=W.input.substring(pe);if(Ie=$e.match(/^\s*=/)){ae.ignoreMatch();return}if((Ie=$e.match(/^\s+extends\s+/))&&Ie.index===0){ae.ignoreMatch();return}}},i={$pattern:Ye,keyword:Ht,literal:Vt,built_in:Jt,"variable.language":Kt},s="[0-9](_?[0-9])*",c=`\\.(${s})`,d="0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*",f={className:"number",variants:[{begin:`(\\b(${d})((${c})|\\.)?|(${c}))[eE][+-]?(${s})\\b`},{begin:`\\b(${d})\\b((${c})\\b|\\.)?|(${c})\\b`},{begin:"\\b(0|[1-9](_?[0-9])*)n\\b"},{begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*n?\\b"},{begin:"\\b0[oO][0-7](_?[0-7])*n?\\b"},{begin:"\\b0[0-7]+n?\\b"}],relevance:0},p={className:"subst",begin:"\\$\\{",end:"\\}",keywords:i,contains:[]},b={begin:".?html`",end:"",starts:{end:"`",returnEnd:!1,contains:[e.BACKSLASH_ESCAPE,p],subLanguage:"xml"}},l={begin:".?css`",end:"",starts:{end:"`",returnEnd:!1,contains:[e.BACKSLASH_ESCAPE,p],subLanguage:"css"}},m={begin:".?gql`",end:"",starts:{end:"`",returnEnd:!1,contains:[e.BACKSLASH_ESCAPE,p],subLanguage:"graphql"}},h={className:"string",begin:"`",end:"`",contains:[e.BACKSLASH_ESCAPE,p]},E={className:"comment",variants:[e.COMMENT(/\/\*\*(?!\/)/,"\\*/",{relevance:0,contains:[{begin:"(?=@[A-Za-z]+)",relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+"},{className:"type",begin:"\\{",end:"\\}",excludeEnd:!0,excludeBegin:!0,relevance:0},{className:"variable",begin:n+"(?=\\s*(-)|$)",endsParent:!0,relevance:0},{begin:/(?=[^\n])\s/,relevance:0}]}]}),e.C_BLOCK_COMMENT_MODE,e.C_LINE_COMMENT_MODE]},A=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,b,l,m,h,{match:/\$\d+/},f];p.contains=A.concat({begin:/\{/,end:/\}/,keywords:i,contains:["self"].concat(A)});const T=[].concat(E,p.contains),S=T.concat([{begin:/(\s*)\(/,end:/\)/,keywords:i,contains:["self"].concat(T)}]),M={className:"params",begin:/(\s*)\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:i,contains:S},L={variants:[{match:[/class/,/\s+/,n,/\s+/,/extends/,/\s+/,u.concat(n,"(",u.concat(/\./,n),")*")],scope:{1:"keyword",3:"title.class",5:"keyword",7:"title.class.inherited"}},{match:[/class/,/\s+/,n],scope:{1:"keyword",3:"title.class"}}]},j={relevance:0,match:u.either(/\bJSON/,/\b[A-Z][a-z]+([A-Z][a-z]*|\d)*/,/\b[A-Z]{2,}([A-Z][a-z]+|\d)+([A-Z][a-z]*)*/,/\b[A-Z]{2,}[a-z]+([A-Z][a-z]+|\d)*([A-Z][a-z]*)*/),className:"title.class",keywords:{_:[...Zt,...Gt]}},Q={label:"use_strict",className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},ue={variants:[{match:[/function/,/\s+/,n,/(?=\s*\()/]},{match:[/function/,/\s*(?=\()/]}],className:{1:"keyword",3:"title.function"},label:"func.def",contains:[M],illegal:/%/},ve={relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/,className:"variable.constant"};function Ne(W){return u.concat("(?!",W.join("|"),")")}const he={match:u.concat(/\b/,Ne([...Wt,"super","import"].map(W=>`${W}\\s*\\(`)),n,u.lookahead(/\s*\(/)),className:"title.function",relevance:0},ye={begin:u.concat(/\./,u.lookahead(u.concat(n,/(?![0-9A-Za-z$_(])/))),end:n,excludeBegin:!0,keywords:"prototype",className:"property",relevance:0},te={match:[/get|set/,/\s+/,n,/(?=\()/],className:{1:"keyword",3:"title.function"},contains:[{begin:/\(\)/},M]},we="(\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)|"+e.UNDERSCORE_IDENT_RE+")\\s*=>",iu={match:[/const|var|let/,/\s+/,n,/\s*/,/=\s*/,/(async\s*)?/,u.lookahead(we)],keywords:"async",className:{1:"keyword",3:"title.function"},contains:[M]};return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:i,exports:{PARAMS_CONTAINS:S,CLASS_REFERENCE:j},illegal:/#(?![$_A-z])/,contains:[e.SHEBANG({label:"shebang",binary:"node",relevance:5}),Q,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,b,l,m,h,E,{match:/\$\d+/},f,j,{scope:"attr",match:n+u.lookahead(":"),relevance:0},iu,{begin:"("+e.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",relevance:0,contains:[E,e.REGEXP_MODE,{className:"function",begin:we,returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:e.UNDERSCORE_IDENT_RE,relevance:0},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/(\s*)\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:i,contains:S}]}]},{begin:/,/,relevance:0},{match:/\s+/,relevance:0},{variants:[{begin:r.begin,end:r.end},{match:o},{begin:a.begin,"on:begin":a.isTrulyOpeningTag,end:a.end}],subLanguage:"xml",contains:[{begin:a.begin,end:a.end,skip:!0,contains:["self"]}]}]},ue,{beginKeywords:"while if switch catch for"},{begin:"\\b(?!function)"+e.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{",returnBegin:!0,label:"func.def",contains:[M,e.inherit(e.TITLE_MODE,{begin:n,className:"title.function"})]},{match:/\.\.\./,relevance:0},ye,{match:"\\$"+n,relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"},contains:[M]},he,ve,L,te,{match:/\$[(.]/}]}}function Jo(e){const u=e.regex,t=Ko(e),n=Ye,r=["any","void","number","boolean","string","object","never","symbol","bigint","unknown"],o={begin:[/namespace/,/\s+/,e.IDENT_RE],beginScope:{1:"keyword",3:"title.class"}},a={beginKeywords:"interface",end:/\{/,excludeEnd:!0,keywords:{keyword:"interface extends",built_in:r},contains:[t.exports.CLASS_REFERENCE]},i={className:"meta",relevance:10,begin:/^\s*['"]use strict['"]/},s=["type","interface","public","private","protected","implements","declare","abstract","readonly","enum","override","satisfies"],c={$pattern:Ye,keyword:Ht.concat(s),literal:Vt,built_in:Jt.concat(r),"variable.language":Kt},d={className:"meta",begin:"@"+n},f=(m,h,g)=>{const E=m.contains.findIndex(A=>A.label===h);if(E===-1)throw new Error("can not find mode to replace");m.contains.splice(E,1,g)};Object.assign(t.keywords,c),t.exports.PARAMS_CONTAINS.push(d);const p=t.contains.find(m=>m.scope==="attr"),b=Object.assign({},p,{match:u.concat(n,u.lookahead(/\s*\?:/))});t.exports.PARAMS_CONTAINS.push([t.exports.CLASS_REFERENCE,p,b]),t.contains=t.contains.concat([d,o,a,b]),f(t,"shebang",e.SHEBANG()),f(t,"use_strict",i);const l=t.contains.find(m=>m.label==="func.def");return l.relevance=0,Object.assign(t,{name:"TypeScript",aliases:["ts","tsx","mts","cts"]}),t}function Qo(e){const u=e.regex,t=u.concat(/[\p{L}_]/u,u.optional(/[\p{L}0-9_.-]*:/u),/[\p{L}0-9_.-]*/u),n=/[\p{L}0-9._:-]+/u,r={className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},o={begin:/\s/,contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]},a=e.inherit(o,{begin:/\(/,end:/\)/}),i=e.inherit(e.APOS_STRING_MODE,{className:"string"}),s=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),c={endsWithParent:!0,illegal:/`]+/}]}]}]};return{name:"HTML, XML",aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],case_insensitive:!0,unicodeRegex:!0,contains:[{className:"meta",begin://,relevance:10,contains:[o,s,i,a,{begin:/\[/,end:/\]/,contains:[{className:"meta",begin://,contains:[o,a,s,i]}]}]},e.COMMENT(//,{relevance:10}),{begin://,relevance:10},r,{className:"meta",end:/\?>/,variants:[{begin:/<\?xml/,relevance:10,contains:[s]},{begin:/<\?[a-z][a-z0-9]+/}]},{className:"tag",begin:/)/,end:/>/,keywords:{name:"style"},contains:[c],starts:{end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:/)/,end:/>/,keywords:{name:"script"},contains:[c],starts:{end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{className:"tag",begin:/<>|<\/>/},{className:"tag",begin:u.concat(//,/>/,/\s/)))),end:/\/?>/,contains:[{className:"name",begin:t,relevance:0,starts:c}]},{className:"tag",begin:u.concat(/<\//,u.lookahead(u.concat(t,/>/))),contains:[{className:"name",begin:t,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]}}const Xo=e=>({IMPORTANT:{scope:"meta",begin:"!important"},BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{scope:"number",begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/},FUNCTION_DISPATCH:{className:"built_in",begin:/[\w-]+(?=\()/},ATTRIBUTE_SELECTOR_MODE:{scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$",contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{scope:"number",begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z_][A-Za-z0-9_-]*/}}),Yo=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","optgroup","option","p","picture","q","quote","samp","section","select","source","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],ei=["defs","g","marker","mask","pattern","svg","switch","symbol","feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feFlood","feGaussianBlur","feImage","feMerge","feMorphology","feOffset","feSpecularLighting","feTile","feTurbulence","linearGradient","radialGradient","stop","circle","ellipse","image","line","path","polygon","polyline","rect","text","use","textPath","tspan","foreignObject","clipPath"],ui=[...Yo,...ei],ti=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"].sort().reverse(),ni=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"].sort().reverse(),ri=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"].sort().reverse(),oi=["accent-color","align-content","align-items","align-self","alignment-baseline","all","anchor-name","animation","animation-composition","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-range","animation-range-end","animation-range-start","animation-timeline","animation-timing-function","appearance","aspect-ratio","backdrop-filter","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-position-x","background-position-y","background-repeat","background-size","baseline-shift","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-end-end-radius","border-end-start-radius","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-start-end-radius","border-start-start-radius","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-align","box-decoration-break","box-direction","box-flex","box-flex-group","box-lines","box-ordinal-group","box-orient","box-pack","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","color-scheme","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","contain-intrinsic-block-size","contain-intrinsic-height","contain-intrinsic-inline-size","contain-intrinsic-size","contain-intrinsic-width","container","container-name","container-type","content","content-visibility","counter-increment","counter-reset","counter-set","cue","cue-after","cue-before","cursor","cx","cy","direction","display","dominant-baseline","empty-cells","enable-background","field-sizing","fill","fill-opacity","fill-rule","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flood-color","flood-opacity","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-optical-sizing","font-palette","font-size","font-size-adjust","font-smooth","font-smoothing","font-stretch","font-style","font-synthesis","font-synthesis-position","font-synthesis-small-caps","font-synthesis-style","font-synthesis-weight","font-variant","font-variant-alternates","font-variant-caps","font-variant-east-asian","font-variant-emoji","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","forced-color-adjust","gap","glyph-orientation-horizontal","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphenate-character","hyphenate-limit-chars","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","initial-letter","initial-letter-align","inline-size","inset","inset-area","inset-block","inset-block-end","inset-block-start","inset-inline","inset-inline-end","inset-inline-start","isolation","justify-content","justify-items","justify-self","kerning","left","letter-spacing","lighting-color","line-break","line-height","line-height-step","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","margin-trim","marker","marker-end","marker-mid","marker-start","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","masonry-auto-flow","math-depth","math-shift","math-style","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","offset","offset-anchor","offset-distance","offset-path","offset-position","offset-rotate","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-anchor","overflow-block","overflow-clip-margin","overflow-inline","overflow-wrap","overflow-x","overflow-y","overlay","overscroll-behavior","overscroll-behavior-block","overscroll-behavior-inline","overscroll-behavior-x","overscroll-behavior-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page","page-break-after","page-break-before","page-break-inside","paint-order","pause","pause-after","pause-before","perspective","perspective-origin","place-content","place-items","place-self","pointer-events","position","position-anchor","position-visibility","print-color-adjust","quotes","r","resize","rest","rest-after","rest-before","right","rotate","row-gap","ruby-align","ruby-position","scale","scroll-behavior","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scroll-timeline","scroll-timeline-axis","scroll-timeline-name","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","shape-rendering","speak","speak-as","src","stop-color","stop-opacity","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","tab-size","table-layout","text-align","text-align-all","text-align-last","text-anchor","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-skip","text-decoration-skip-ink","text-decoration-style","text-decoration-thickness","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-size-adjust","text-transform","text-underline-offset","text-underline-position","text-wrap","text-wrap-mode","text-wrap-style","timeline-scope","top","touch-action","transform","transform-box","transform-origin","transform-style","transition","transition-behavior","transition-delay","transition-duration","transition-property","transition-timing-function","translate","unicode-bidi","user-modify","user-select","vector-effect","vertical-align","view-timeline","view-timeline-axis","view-timeline-inset","view-timeline-name","view-transition-name","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","white-space-collapse","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","x","y","z-index","zoom"].sort().reverse();function ii(e){const u=e.regex,t=Xo(e),n={begin:/-(webkit|moz|ms|o)-(?=[a-z])/},r="and or not only",o=/@-?\w[\w]*(-\w+)*/,a="[a-zA-Z-][a-zA-Z0-9_-]*",i=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE];return{name:"CSS",case_insensitive:!0,illegal:/[=|'\$]/,keywords:{keyframePosition:"from to"},classNameAliases:{keyframePosition:"selector-tag"},contains:[t.BLOCK_COMMENT,n,t.CSS_NUMBER_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0},{className:"selector-class",begin:"\\."+a,relevance:0},t.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{begin:":("+ni.join("|")+")"},{begin:":(:)?("+ri.join("|")+")"}]},t.CSS_VARIABLE,{className:"attribute",begin:"\\b("+oi.join("|")+")\\b"},{begin:/:/,end:/[;}{]/,contains:[t.BLOCK_COMMENT,t.HEXCOLOR,t.IMPORTANT,t.CSS_NUMBER_MODE,...i,{begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri"},contains:[...i,{className:"string",begin:/[^)]/,endsWithParent:!0,excludeEnd:!0}]},t.FUNCTION_DISPATCH]},{begin:u.lookahead(/@/),end:"[{;]",relevance:0,illegal:/:/,contains:[{className:"keyword",begin:o},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:{$pattern:/[a-z-]+/,keyword:r,attribute:ti.join(" ")},contains:[{begin:/[a-z-]+(?=:)/,className:"attribute"},...i,t.CSS_NUMBER_MODE]}]},{className:"selector-tag",begin:"\\b("+ui.join("|")+")\\b"}]}}function ai(e){const u="true false yes no null",t="[\\w#;/?:@&=+$,.~*'()[\\]]+",n={className:"attr",variants:[{begin:/[\w*@][\w*@ :()\./-]*:(?=[ \t]|$)/},{begin:/"[\w*@][\w*@ :()\./-]*":(?=[ \t]|$)/},{begin:/'[\w*@][\w*@ :()\./-]*':(?=[ \t]|$)/}]},r={className:"template-variable",variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]},o={className:"string",relevance:0,begin:/'/,end:/'/,contains:[{match:/''/,scope:"char.escape",relevance:0}]},a={className:"string",relevance:0,variants:[{begin:/"/,end:/"/},{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,r]},i=e.inherit(a,{variants:[{begin:/'/,end:/'/,contains:[{begin:/''/,relevance:0}]},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),p={className:"number",begin:"\\b"+"[0-9]{4}(-[0-9][0-9]){0,2}"+"([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?"+"(\\.[0-9]*)?"+"([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?"+"\\b"},b={end:",",endsWithParent:!0,excludeEnd:!0,keywords:u,relevance:0},l={begin:/\{/,end:/\}/,contains:[b],illegal:"\\n",relevance:0},m={begin:"\\[",end:"\\]",contains:[b],illegal:"\\n",relevance:0},h=[n,{className:"meta",begin:"^---\\s*$",relevance:10},{className:"string",begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!\\w+!"+t},{className:"type",begin:"!<"+t+">"},{className:"type",begin:"!"+t},{className:"type",begin:"!!"+t},{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)",relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:u,keywords:{literal:u}},p,{className:"number",begin:e.C_NUMBER_RE+"\\b",relevance:0},l,m,o,a],g=[...h];return g.pop(),g.push(i),b.contains=g,{name:"YAML",case_insensitive:!0,aliases:["yml"],contains:h}}ne.registerLanguage("javascript",Dn);ne.registerLanguage("typescript",Jo);ne.registerLanguage("python",Fn);ne.registerLanguage("json",Sn);ne.registerLanguage("bash",Tn);ne.registerLanguage("xml",Qo);ne.registerLanguage("css",ii);ne.registerLanguage("yaml",ai);const si={js:"javascript",ts:"typescript",py:"python",sh:"bash",shell:"bash",yml:"yaml",html:"xml"};function ci(e){return e.replace(/&/g,"&").replace(//g,">")}function ot(e,u){const t=si[(u||"").toLowerCase()]||(u||"").toLowerCase();if(t&&ne.getLanguage(t))try{return ne.highlight(e,{language:t,ignoreIllegals:!0}).value}catch{}return ci(e)}const li={class:"cb"},di={class:"cb-bar"},fi={class:"cb-lang"},pi=["title"],bi={class:"cb-pre"},hi=["innerHTML"],mi={key:0,class:"cb-fade"},gi={__name:"CodeBlock",props:{code:{type:String,required:!0},lang:{type:String,default:""},collapseAfter:{type:Number,default:24}},setup(e){const u=e,t=G(()=>u.code.split(` +`).length),n=G(()=>t.value>u.collapseAfter),r=R(n.value),o=R(ot(u.code,u.lang));let a=null,i=0;ke(()=>u.code,()=>{const f=Date.now()-i,p=()=>{i=Date.now(),o.value=ot(u.code,u.lang)};f>=80?p():(clearTimeout(a),a=setTimeout(p,80-f))}),st(()=>clearTimeout(a));const s=R(!1);let c=null;async function d(){await ft(u.code)&&(s.value=!0,clearTimeout(c),c=setTimeout(()=>{s.value=!1},1200))}return(f,p)=>(k(),C("div",li,[x("div",di,[x("span",fi,$(e.lang||"text"),1),x("button",{class:"cb-copy",type:"button",title:s.value?"Copied":"Copy code",onClick:d},[(k(),U(Le(s.value?v(Be):v(pt)),{class:"w-3 h-3"})),P(" "+$(s.value?"Copied":"Copy"),1)],8,pi)]),x("div",{class:z(["cb-scroll",{"cb-clamped":r.value}])},[x("pre",bi,[x("code",{class:"hljs",innerHTML:o.value},null,8,hi)]),r.value?(k(),C("div",mi)):N("",!0)],2),n.value?(k(),C("button",{key:0,class:"cb-expand",type:"button",onClick:p[0]||(p[0]=b=>r.value=!r.value)},[(k(),U(Le(r.value?v(On):v(Bn)),{class:"w-3 h-3"})),P(" "+$(r.value?`Show all ${t.value} lines`:"Collapse"),1)])):N("",!0)]))}},yu=eu(gi,[["__scopeId","data-v-a1ccbe31"]]),xi={class:"rounded-lg border border-border/70 bg-surface/40"},ki=["aria-expanded"],_i={key:0,class:"px-3 pb-2.5"},vi={class:"border-t border-border/70 pt-2 text-xs leading-relaxed text-foreground-muted font-mono whitespace-pre-wrap break-words"},yi={__name:"ThinkingBlock",props:{part:{type:Object,required:!0}},setup(e){const u=e,t=G(()=>!!u.part.streaming),n=R(Date.now()),r=R(null);let o=null;const a=G(()=>r.value!=null?r.value:u.part.startedAt==null?null:Math.max(0,Math.round((n.value-u.part.startedAt)/1e3))),i=G(()=>{if(t.value){const l=a.value;return l!=null?`Thinking… ${l}s`:"Thinking…"}const b=a.value;return b!=null?`Thought for ${b}s`:"Reasoning"}),s=R(!1),c=R(t.value);function d(){s.value=!0,c.value=!c.value}function f(){p(),o=setInterval(()=>{n.value=Date.now()},1e3)}function p(){o&&(clearInterval(o),o=null)}return ke(t,(b,l)=>{b?(s.value||(c.value=!0),f()):(u.part.startedAt!=null&&(r.value=Math.max(0,Math.round((Date.now()-u.part.startedAt)/1e3))),p(),l&&!s.value&&(c.value=!1))}),Se(()=>{t.value&&f()}),Cu(p),(b,l)=>(k(),C("div",xi,[x("button",{type:"button",class:"flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary","aria-expanded":c.value,onClick:d},[D(v(ht),{class:z(["w-3.5 h-3.5 shrink-0",t.value?"text-primary":"text-foreground-muted"])},null,8,["class"]),x("span",{class:z(["text-xs font-medium",t.value?"thinking-shimmer text-foreground":"text-foreground-muted"])},$(i.value),3),l[0]||(l[0]=x("span",{class:"flex-1"},null,-1)),D(v(wu),{class:z(["w-3.5 h-3.5 shrink-0 text-foreground-muted transition-transform duration-150",{"rotate-180":c.value}])},null,8,["class"])],8,ki),c.value?(k(),C("div",_i,[x("div",vi,$(e.part.text),1)])):N("",!0)]))}},Ei=eu(yi,[["__scopeId","data-v-d5f3c44f"]]),Ci={key:0},wi=["innerHTML"],Ai={__name:"MessagePart",props:{part:{type:Object,required:!0}},setup(e){const u=e,t=new re({html:!1,linkify:!0,breaks:!0,highlight(i,s){if(s&&ne.getLanguage(s))try{return ne.highlight(i,{language:s,ignoreIllegals:!0}).value}catch{}return""}});function n(i){i=i||"";let s;try{s=t.parse(i,{})}catch{return[{type:"html",html:t.render(i)}]}const c=[];let d=[];const f=()=>{d.length&&(c.push({type:"html",html:t.renderer.render(d,t.options,{})}),d=[])};for(const p of s)p.type==="fence"?(f(),c.push({type:"code",code:p.content.replace(/\n$/,""),lang:(p.info||"").trim().split(/\s+/)[0]})):d.push(p);return f(),c}const r=R(n(u.part.text));let o=null,a=0;return ke(()=>u.part.text,()=>{const i=Date.now()-a,s=()=>{a=Date.now(),r.value=n(u.part.text)};i>=80?s():(clearTimeout(o),o=setTimeout(s,80-i))}),st(()=>clearTimeout(o)),(i,s)=>e.part.type==="text"?(k(),C("div",Ci,[(k(!0),C(q,null,Y(r.value,(c,d)=>(k(),C(q,{key:d},[c.type==="code"?(k(),U(yu,{key:0,code:c.code,lang:c.lang},null,8,["code","lang"])):(k(),C("div",{key:1,class:"md-body",innerHTML:c.html},null,8,wi))],64))),128))])):e.part.type==="thinking"?(k(),U(Ei,{key:1,part:e.part},null,8,["part"])):N("",!0)}},it=eu(Ai,[["__scopeId","data-v-2f8b301c"]]),Di={key:0,class:"flex max-w-[85%] flex-col items-end"},Fi={class:"mt-1.5 flex items-center gap-2"},Si={class:"rounded-2xl rounded-br-md bg-primary px-3.5 py-2.5 text-sm leading-relaxed text-primary-foreground"},Ti={class:"mt-1 flex items-center gap-1"},Ri={key:1,class:"w-full text-sm leading-relaxed text-foreground"},Mi={class:"mt-1.5 flex items-center gap-1"},Ni=["title"],ze="inline-flex items-center rounded-md px-1.5 py-1 text-[11px] text-foreground-muted opacity-0 transition-[opacity,color,background-color] hover:bg-surface-hover hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background group-hover:opacity-100 max-md:opacity-100",Ii={__name:"Message",props:{role:{type:String,required:!0,validator:e=>e==="user"||e==="assistant"},parts:{type:Array,default:()=>[]},canRegenerate:{type:Boolean,default:!1},editable:{type:Boolean,default:!1},deletable:{type:Boolean,default:!1}},emits:["regenerate","edit","delete"],setup(e,{emit:u}){const t=e,n=u,r=G(()=>t.parts.filter(h=>h.type==="text"&&h.text).map(h=>h.text).join(` + +`).trim()),o=R(!1);let a=null;async function i(){await ft(r.value)&&(o.value=!0,clearTimeout(a),a=setTimeout(()=>{o.value=!1},1200))}const s=R(!1),c=R(null),d=R(!1);function f(){s.value=!0,Ce(()=>{const h=c.value;if(!h)return;h.textContent=r.value,d.value=!r.value.trim(),h.focus();const g=document.createRange();g.selectNodeContents(h),g.collapse(!1);const E=window.getSelection();E.removeAllRanges(),E.addRange(g)})}function p(){d.value=!(c.value?.innerText||"").trim()}function b(){s.value=!1}function l(){const h=(c.value?.innerText||"").trim();h&&(s.value=!1,n("edit",h))}function m(h){h.key==="Enter"&&!h.shiftKey?(h.preventDefault(),l()):h.key==="Escape"&&(h.preventDefault(),b())}return(h,g)=>(k(),C("div",{class:z(["group flex",{"justify-end":e.role==="user"}])},[e.role==="user"?(k(),C("div",Di,[s.value?(k(),C(q,{key:0},[x("div",{ref_key:"editBox",ref:c,contenteditable:"plaintext-only",role:"textbox","aria-multiline":"true","aria-label":"Edit message",class:"max-w-full whitespace-pre-wrap break-words rounded-2xl rounded-br-md border border-border bg-surface px-3.5 py-2.5 text-sm leading-relaxed text-foreground outline-none transition-colors focus:border-white focus:ring-1 focus:ring-white",onInput:p,onKeydown:m},null,544),x("div",Fi,[D(K,{size:"xs",variant:"ghost",onClick:b},{default:O(()=>[...g[3]||(g[3]=[P(" Cancel ",-1)])]),_:1}),D(K,{size:"xs",variant:"primary",disabled:d.value,onClick:l},{default:O(()=>[...g[4]||(g[4]=[P(" Send ",-1)])]),_:1},8,["disabled"])])],64)):(k(),C(q,{key:1},[x("div",Si,[(k(!0),C(q,null,Y(e.parts,(E,A)=>(k(),U(it,{key:`${E.type}-${A}`,part:E},null,8,["part"]))),128))]),x("div",Ti,[e.editable?(k(),C("button",{key:0,type:"button",class:z(ze),"aria-label":"Edit and resend",title:"Edit and resend",onClick:f},[D(v(dt),{class:"h-3 w-3"})])):N("",!0),e.deletable?(k(),C("button",{key:1,type:"button",class:z(ze),"aria-label":"Delete from here",title:"Delete from here",onClick:g[0]||(g[0]=E=>n("delete"))},[D(v(ku),{class:"h-3 w-3"})])):N("",!0)])],64))])):(k(),C("div",Ri,[(k(!0),C(q,null,Y(e.parts,(E,A)=>(k(),U(it,{key:`${E.type}-${A}`,part:E,class:z(A>0?"mt-3":"")},null,8,["part","class"]))),128)),x("div",Mi,[r.value?(k(),C("button",{key:0,type:"button",class:z([ze,"gap-1"]),title:o.value?"Copied":"Copy message",onClick:i},[(k(),U(Le(o.value?v(Be):v(pt)),{class:"h-3 w-3"})),P(" "+$(o.value?"Copied":"Copy"),1)],10,Ni)):N("",!0),e.canRegenerate?(k(),C("button",{key:1,type:"button",class:z([ze,"gap-1"]),title:"Regenerate this answer",onClick:g[1]||(g[1]=E=>n("regenerate"))},[D(v(bt),{class:"h-3 w-3"}),g[5]||(g[5]=P(" Retry ",-1))],2)):N("",!0),e.deletable?(k(),C("button",{key:2,type:"button",class:z(ze),"aria-label":"Delete from here",title:"Delete from here",onClick:g[2]||(g[2]=E=>n("delete"))},[D(v(ku),{class:"h-3 w-3"})])):N("",!0)])]))],2))}},$i=["data-approval"],zi=["aria-expanded"],Li={class:"truncate font-mono text-foreground"},Bi={key:0,class:"shrink-0 rounded bg-surface-hover px-1.5 py-0.5 text-[10px] uppercase tracking-label text-foreground-muted"},Oi={key:0,class:"border-t border-border px-3 py-3"},Pi={key:1,class:"flex gap-2 border-t border-border px-3 py-2"},qi={__name:"ToolCallCard",props:{tool:{type:Object,required:!0}},emits:["approve","reject"],setup(e){const u=e,t=G(()=>u.tool.status),n={pending_approval:{label:"needs approval",classes:"bg-warning-tint text-warning-fg"},running:{label:"running…",classes:"bg-info-tint text-info-fg"},succeeded:{label:"done",classes:"bg-success-tint text-success-fg"},failed:{label:"failed",classes:"bg-danger-tint text-danger-fg"},rejected:{label:"rejected",classes:"bg-surface-hover text-foreground-muted"}},r=G(()=>n[t.value]??{label:t.value??"unknown",classes:"bg-surface-hover text-foreground-muted"});function o(l){if(l==null)return"";if(typeof l=="string")try{return JSON.stringify(JSON.parse(l),null,2)}catch{return l}return JSON.stringify(l,null,2)}const a=G(()=>o(u.tool.args)),i=G(()=>o(u.tool.result)),s=l=>l==="running"||l==="pending_approval"||l==="failed",c=R(!1),d=R(s(t.value));function f(){c.value=!0,d.value=!d.value}ke(t,l=>{c.value||(d.value=s(l))});const p=R(null);function b(){Ce(()=>{const l=p.value?.$el;l&&(l.scrollIntoView({behavior:"smooth",block:"center"}),l.focus?.())})}return Se(()=>{t.value==="pending_approval"&&b()}),ke(t,l=>{l==="pending_approval"&&b()}),(l,m)=>(k(),C("div",{class:"overflow-hidden rounded-lg border border-border bg-surface text-sm","data-approval":e.tool.status==="pending_approval"?"pending":null},[x("button",{type:"button",class:"flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left transition-colors hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary","aria-expanded":d.value,onClick:f},[D(v(jn),{class:"h-3.5 w-3.5 shrink-0 text-foreground-muted","aria-hidden":"true"}),x("span",Li,$(e.tool.name),1),e.tool.group?(k(),C("span",Bi,$(e.tool.group),1)):N("",!0),m[2]||(m[2]=x("span",{class:"flex-1"},null,-1)),x("span",{class:z(["shrink-0 rounded-full px-2 py-0.5 text-xs",[r.value.classes,t.value==="running"?"animate-pulse motion-reduce:animate-none":""]])},$(r.value.label),3),D(v(wu),{class:z(["h-3.5 w-3.5 shrink-0 text-foreground-muted transition-transform duration-150",{"rotate-180":d.value}])},null,8,["class"])],8,zi),d.value?(k(),C("div",Oi,[m[4]||(m[4]=x("p",{class:"mb-1 text-[10px] uppercase tracking-label text-foreground-muted"}," Arguments ",-1)),D(yu,{code:a.value||"(none)",lang:"json"},null,8,["code"]),e.tool.result!=null?(k(),C(q,{key:0},[m[3]||(m[3]=x("p",{class:"mb-1 mt-3 text-[10px] uppercase tracking-label text-foreground-muted"}," Result ",-1)),D(yu,{code:i.value,lang:"json"},null,8,["code"])],64)):N("",!0)])):N("",!0),e.tool.status==="pending_approval"?(k(),C("div",Pi,[D(K,{ref_key:"approveBtn",ref:p,size:"xs",variant:"primary",onClick:m[0]||(m[0]=h=>l.$emit("approve",e.tool.id))},{default:O(()=>[...m[5]||(m[5]=[P(" Approve ",-1)])]),_:1},512),D(K,{size:"xs",variant:"ghost",onClick:m[1]||(m[1]=h=>l.$emit("reject",e.tool.id))},{default:O(()=>[...m[6]||(m[6]=[P(" Reject ",-1)])]),_:1})])):N("",!0)],8,$i))}},Ui={class:"flex items-start gap-2.5 rounded-lg border border-danger-ring bg-danger-tint px-3.5 py-3 text-sm",role:"alert"},ji={class:"min-w-0 flex-1"},Hi={class:"mt-0.5 break-words text-foreground-muted"},Vi={class:"mt-2.5 flex items-center gap-2"},Zi={__name:"ErrorCard",props:{message:{type:String,default:""}},emits:["retry","dismiss"],setup(e){return(u,t)=>(k(),C("div",Ui,[D(v(gn),{class:"mt-0.5 h-4 w-4 shrink-0 text-danger-fg","aria-hidden":"true"}),x("div",ji,[t[4]||(t[4]=x("p",{class:"font-medium text-danger-fg"}," Something went wrong ",-1)),x("p",Hi,$(e.message),1),x("div",Vi,[D(K,{size:"xs",variant:"secondary",onClick:t[0]||(t[0]=n=>u.$emit("retry"))},{default:O(()=>[D(v(bt),{class:"h-3.5 w-3.5"}),t[2]||(t[2]=P(" Retry ",-1))]),_:1}),D(K,{size:"xs",variant:"ghost",onClick:t[1]||(t[1]=n=>u.$emit("dismiss"))},{default:O(()=>[...t[3]||(t[3]=[P(" Dismiss ",-1)])]),_:1})])])]))}},Gi={},Wi={class:"flex items-center gap-1.5 py-1",role:"status","aria-label":"Assistant is responding"};function Ki(e,u){return k(),C("div",Wi,[...u[0]||(u[0]=[x("span",{class:"dot"},null,-1),x("span",{class:"dot",style:{"animation-delay":"0.18s"}},null,-1),x("span",{class:"dot",style:{"animation-delay":"0.36s"}},null,-1),x("span",{class:"sr-only"},"Assistant is responding…",-1)])])}const Ji=eu(Gi,[["render",Ki],["__scopeId","data-v-3a0eeb7e"]]),Qi={__name:"ScrollToBottom",props:{visible:{type:Boolean,default:!1}},emits:["click"],setup(e){return(u,t)=>(k(),U(ct,{name:"fade"},{default:O(()=>[e.visible?(k(),C("button",{key:0,type:"button",class:"absolute bottom-3 left-1/2 -translate-x-1/2 z-10 flex items-center gap-1.5 rounded-full border border-border bg-surface px-3 py-1.5 text-xs text-foreground-muted shadow-lg shadow-black/30 transition-colors hover:text-white hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background","aria-label":"Scroll to latest",onClick:t[0]||(t[0]=n=>u.$emit("click"))},[D(v($n),{class:"h-3.5 w-3.5"}),t[1]||(t[1]=P(" Latest ",-1))])):N("",!0)]),_:1}))}},Xi={class:"relative min-h-0 flex-1"},Yi={class:"mx-auto w-full max-w-3xl py-6"},ea={__name:"MessageList",props:{items:{type:Array,required:!0},streaming:{type:Boolean,default:!1}},emits:["approve","reject","regenerate","edit","delete","retry","dismiss"],setup(e){const u=e,t=G(()=>{for(let p=u.items.length-1;p>=0;p--){const b=u.items[p];if(b.kind==="message"&&b.role==="assistant")return p}return-1}),n=G(()=>{if(!u.streaming)return!1;const p=u.items[u.items.length-1];return p?p.kind==="tool"?!0:p.kind==="message"?p.role==="user"?!0:!(p.parts||[]).some(l=>l.type==="text"&&l.text||l.type==="thinking"&&l.text):!1:!1}),r=R(null),o=R(!0);function a(p){if(p===0)return"";const b=u.items[p],l=u.items[p-1];return b.kind==="message"&&b.role==="user"?"mt-8":b.kind==="tool"?"mt-2":l.kind==="tool"?"mt-3":"mt-4"}function i(){const p=u.items[u.items.length-1];if(!p||p.kind!=="message"||!Array.isArray(p.parts))return 0;let b=0;for(const l of p.parts)b+=l&&typeof l.text=="string"?l.text.length:0;return b}function s(){const p=r.value;return p?p.scrollTop+p.clientHeight>=p.scrollHeight-100:!0}function c(){o.value=s()}async function d(){await Ce();const p=r.value;p&&(p.scrollTop=p.scrollHeight)}function f(){o.value=!0,d()}return ke(()=>[u.items.length,u.streaming,i()],()=>{o.value&&d()}),Se(d),(p,b)=>(k(),C("div",Xi,[x("div",{ref_key:"scroller",ref:r,class:"scrollable h-full overflow-y-auto px-4",role:"log","aria-live":"polite","aria-atomic":"false","aria-label":"Chat messages",onScroll:c},[x("div",Yi,[(k(!0),C(q,null,Y(e.items,(l,m)=>(k(),C(q,{key:l.id??m},[l.kind==="message"?(k(),U(Ii,{key:0,role:l.role,parts:l.parts,"can-regenerate":l.role==="assistant"&&!e.streaming&&m===t.value,editable:l.role==="user"&&!e.streaming,deletable:!e.streaming&&!!l.id,class:z(a(m)),onRegenerate:b[0]||(b[0]=h=>p.$emit("regenerate")),onEdit:h=>p.$emit("edit",{id:l.id,content:h}),onDelete:h=>p.$emit("delete",l.id)},null,8,["role","parts","can-regenerate","editable","deletable","class","onEdit","onDelete"])):l.kind==="tool"?(k(),U(qi,{key:1,tool:l,class:z(a(m)),onApprove:b[1]||(b[1]=h=>p.$emit("approve",h)),onReject:b[2]||(b[2]=h=>p.$emit("reject",h))},null,8,["tool","class"])):l.kind==="error"?(k(),U(Zi,{key:2,message:l.message,class:z(a(m)),onRetry:b[3]||(b[3]=h=>p.$emit("retry")),onDismiss:h=>p.$emit("dismiss",l.id)},null,8,["message","class","onDismiss"])):N("",!0)],64))),128)),n.value?(k(),U(Ji,{key:0,class:z(e.items.length?"mt-4":"")},null,8,["class"])):N("",!0)])],544),D(Qi,{visible:!o.value,onClick:f},null,8,["visible"])]))}},ua={class:"py-1"},Qt={__name:"Popover",props:{title:{type:String,default:""}},setup(e,{expose:u}){const t=R(!1),n=R(null),r=R(null),o=R({}),a=R(!1);let i=null;const s=()=>{i&&(a.value=i.matches)};function c(){const m=n.value?.firstElementChild||n.value;if(!m)return;const h=m.getBoundingClientRect(),g=Math.max(8,Math.min(h.left,window.innerWidth-8-200));o.value={left:`${g}px`,bottom:`${window.innerHeight-h.top+6}px`},Ce(()=>{const E=r.value;if(!E)return;const A=E.offsetWidth,T=Math.max(8,Math.min(h.left,window.innerWidth-8-A));o.value={...o.value,left:`${T}px`}})}function d(){t.value=!0,a.value||Ce(c)}function f(){t.value=!1}function p(){t.value?f():d()}function b(m){!t.value||a.value||r.value?.contains(m.target)||n.value?.contains(m.target)||f()}function l(){t.value&&!a.value&&c()}return Se(()=>{i=window.matchMedia("(max-width: 639px)"),s(),i.addEventListener("change",s),document.addEventListener("pointerdown",b,!0),window.addEventListener("resize",l)}),Cu(()=>{i?.removeEventListener("change",s),document.removeEventListener("pointerdown",b,!0),window.removeEventListener("resize",l)}),u({open:d,close:f}),(m,h)=>(k(),C(q,null,[x("span",{ref_key:"triggerEl",ref:n,class:"inline-flex"},[lu(m.$slots,"trigger",{open:t.value,toggle:p})],512),(k(),U(_n,{to:"body"},[D(ct,{name:"fade"},{default:O(()=>[t.value&&!a.value?(k(),C("div",{key:0,ref_key:"panelEl",ref:r,class:"fixed z-50 min-w-[200px] max-w-[min(320px,calc(100vw-1rem))] bg-background border border-border rounded-lg shadow-xl overflow-hidden",style:kn(o.value),role:"menu",onKeydown:xn(f,["esc"])},[lu(m.$slots,"default",{close:f})],36)):N("",!0)]),_:3})])),a.value?(k(),U(lt,{key:0,modelValue:t.value,"onUpdate:modelValue":h[0]||(h[0]=g=>t.value=g),title:e.title},{default:O(()=>[x("div",ua,[lu(m.$slots,"default",{close:f})])]),_:3},8,["modelValue","title"])):N("",!0)],64))}},ta=["title","onClick"],na={key:0},ra={class:"py-1"},oa=["aria-checked","onClick"],ia={class:"flex-1"},aa={class:"block"},sa={class:"block text-[11px] text-foreground-muted"},ca={key:0,class:"px-3 pt-1.5 pb-1.5 mt-1 border-t border-border text-[11px] text-foreground-muted leading-snug"},la={__name:"ReasoningMenu",setup(e){const u=He(),t=[{v:"off",label:"Off",hint:"Answer directly",icon:Ln},{v:"standard",label:"Think",hint:"Reason before answering",icon:Rn},{v:"deep",label:"Deep",hint:"Extended reasoning",icon:Mn}],n=G(()=>u.thinking&&u.thinking!=="off"),r=G(()=>t.find(s=>s.v===u.thinking)?.label||"Off"),o=[/\bo[1-9]/i,/gpt-5/i,/-thinking/i,/reason/i,/deepseek-r/i,/claude.*(sonnet|opus)/i,/qwen.*(think|3)/i,/grok.*(3|4)/i],a=G(()=>{const s=u.selectedModel||"";return o.some(c=>c.test(s))});function i(s,c){u.setThinking(s),c()}return(s,c)=>(k(),U(Qt,{title:"Reasoning"},{trigger:O(({toggle:d})=>[x("button",{type:"button",class:z(["inline-flex h-8 items-center justify-center gap-1.5 rounded-lg px-2.5 text-xs transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-surface",n.value?"text-primary bg-primary/15 hover:bg-primary/20":"text-foreground-muted hover:text-foreground hover:bg-surface-hover"]),title:`Reasoning: ${r.value}`,"aria-label":"Reasoning level",onClick:d},[D(v(ht),{class:"w-4 h-4 shrink-0"}),n.value?(k(),C("span",na,$(r.value),1)):N("",!0)],10,ta)]),default:O(({close:d})=>[x("div",ra,[c[0]||(c[0]=x("p",{class:"px-3 pt-1.5 pb-1 text-[10px] uppercase tracking-label text-foreground-muted"}," Reasoning ",-1)),(k(),C(q,null,Y(t,f=>x("button",{key:f.v,type:"button",class:z(["flex w-full items-center gap-2.5 px-3 py-2 text-left text-sm transition-colors focus-visible:outline-none focus-visible:bg-surface-hover",v(u).thinking===f.v?"text-white bg-primary/15":"text-foreground hover:bg-surface-hover"]),role:"menuitemradio","aria-checked":v(u).thinking===f.v,onClick:p=>i(f.v,d)},[(k(),U(Le(f.icon),{class:z(["w-3.5 h-3.5 shrink-0",v(u).thinking===f.v?"text-primary":"text-foreground-muted"])},null,8,["class"])),x("span",ia,[x("span",aa,$(f.label),1),x("span",sa,$(f.hint),1)]),v(u).thinking===f.v?(k(),U(v(Be),{key:0,class:"w-3.5 h-3.5 shrink-0 text-primary"})):N("",!0)],10,oa)),64)),n.value&&!a.value?(k(),C("p",ca,$(v(u).selectedModel||"This model")+" may ignore reasoning requests. ",1)):N("",!0)])]),_:1}))}},da=["title","onClick"],fa={class:"truncate font-mono"},pa={class:"max-h-[60dvh] overflow-y-auto scrollable py-1"},ba=["onClick"],ha={class:"flex-1 truncate"},ma={key:1,class:"px-3 py-2 text-sm text-foreground-muted"},ga={key:2,class:"px-3 py-2 text-xs text-foreground-muted leading-snug"},xa=["onClick"],ka={class:"flex-1 truncate"},_a={__name:"ModelMenu",setup(e){const u=He();function t(n,r){u.selectModel(n),r()}return(n,r)=>(k(),U(Qt,{title:"Model"},{trigger:O(({toggle:o})=>[x("button",{type:"button",class:"inline-flex items-center gap-1.5 h-8 max-w-[180px] px-2.5 rounded-lg text-xs text-foreground-muted hover:text-foreground hover:bg-surface-hover transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-surface",title:v(u).selectedModel||"Select model","aria-label":"Select model",onClick:o},[D(v(Pn),{class:"w-3.5 h-3.5 shrink-0"}),x("span",fa,$(v(u).selectedModel||"No model"),1),D(v(wu),{class:"w-3 h-3 shrink-0 opacity-70"})],8,da)]),default:O(({close:o})=>[x("div",pa,[v(u).providers.length>1?(k(),C(q,{key:0},[r[0]||(r[0]=x("p",{class:"px-3 pt-1.5 pb-1 text-[10px] uppercase tracking-label text-foreground-muted"}," Provider ",-1)),(k(!0),C(q,null,Y(v(u).providers,a=>(k(),C("button",{key:a.id,type:"button",class:z(["flex w-full items-center gap-2.5 px-3 py-1.5 text-left text-sm transition-colors focus-visible:outline-none focus-visible:bg-surface-hover",v(u).selectedProviderId===a.id?"text-white bg-primary/15":"text-foreground hover:bg-surface-hover"]),onClick:i=>v(u).selectProvider(a.id)},[x("span",ha,$(a.label||a.provider),1),v(u).selectedProviderId===a.id?(k(),U(v(Be),{key:0,class:"w-3.5 h-3.5 shrink-0 text-primary"})):N("",!0)],10,ba))),128)),r[1]||(r[1]=x("div",{class:"my-1 border-t border-border"},null,-1))],64)):N("",!0),r[2]||(r[2]=x("p",{class:"px-3 pt-1 pb-1 text-[10px] uppercase tracking-label text-foreground-muted"}," Model ",-1)),v(u).modelsLoading?(k(),C("p",ma," Loading models… ")):v(u).models.length?(k(!0),C(q,{key:3},Y(v(u).models,a=>(k(),C("button",{key:a.id,type:"button",class:z(["flex w-full items-center gap-2.5 px-3 py-1.5 text-left text-sm font-mono transition-colors focus-visible:outline-none focus-visible:bg-surface-hover",v(u).selectedModel===a.id?"text-white bg-primary/15":"text-foreground hover:bg-surface-hover"]),onClick:i=>t(a.id,o)},[x("span",ka,$(a.id),1),v(u).selectedModel===a.id?(k(),U(v(Be),{key:0,class:"w-3.5 h-3.5 shrink-0 text-primary"})):N("",!0)],10,xa))),128)):(k(),C("p",ga,$(v(u).modelsError?"No models. Check the provider endpoint.":"No models reported."),1))])]),_:1}))}},va={class:"mx-auto w-full max-w-3xl"},ya=["disabled","placeholder"],Ea={class:"flex items-center gap-1.5 px-2 pb-2"},Ca=["disabled"],at={__name:"Composer",props:{streaming:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},docked:{type:Boolean,default:!0},modelReady:{type:Boolean,default:!0}},emits:["send","stop"],setup(e,{expose:u,emit:t}){const n=e,r=t,o=R(""),a=R(null),i=G(()=>n.disabled?"Configure a provider to start":"Message the assistant…");function s(){const p=a.value;p&&(p.style.height="auto",p.style.height=`${p.scrollHeight}px`)}function c(){const p=o.value.trim();!p||n.streaming||n.disabled||!n.modelReady||(r("send",p),o.value="",Ce(()=>{a.value&&(a.value.style.height="auto")}))}function d(p){p.key==="Enter"&&!p.shiftKey&&(p.preventDefault(),c())}function f(p){o.value=p,Ce(()=>{s(),a.value?.focus()})}return u({focus:()=>a.value?.focus(),setText:f}),(p,b)=>(k(),U(Le(e.docked?"footer":"div"),{class:z(["shrink-0 px-4",e.docked?"pt-3 pb-composer":"py-0"])},{default:O(()=>[x("div",va,[x("div",{class:z(["rounded-2xl border border-border bg-surface transition-colors focus-within:border-foreground-muted/50",{"opacity-60":e.disabled}])},[xu(x("textarea",{ref_key:"ta",ref:a,"onUpdate:modelValue":b[0]||(b[0]=l=>o.value=l),rows:"1",disabled:e.disabled,placeholder:i.value,"aria-label":"Message the assistant",class:"max-h-40 w-full resize-none overflow-y-auto bg-transparent px-3.5 pt-3 pb-1.5 text-sm text-foreground placeholder-foreground-muted/60 focus:outline-none disabled:cursor-not-allowed",onInput:s,onKeydown:d},null,40,ya),[[vn,o.value]]),x("div",Ea,[e.disabled?N("",!0):(k(),C(q,{key:0},[D(la),D(_a)],64)),b[2]||(b[2]=x("span",{class:"flex-1"},null,-1)),e.streaming?(k(),C("button",{key:1,type:"button",class:"touch-expand-iconbtn inline-flex h-8 w-8 items-center justify-center rounded-lg bg-surface-hover text-foreground transition-colors hover:bg-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-surface","aria-label":"Stop generating (Esc)",title:"Stop generating (Esc)",onClick:b[1]||(b[1]=l=>p.$emit("stop"))},[D(v(Un),{class:"h-3.5 w-3.5 fill-current"})])):(k(),C("button",{key:2,type:"button",class:"touch-expand-iconbtn inline-flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground shadow-sm transition-colors hover:bg-primary-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-surface disabled:opacity-40 disabled:shadow-none",disabled:e.disabled||!e.modelReady||!o.value.trim(),"aria-label":"Send message",title:"Send message",onClick:c},[D(v(zn),{class:"h-4 w-4"})],8,Ca))])],2)])]),_:1},8,["class"]))}},wa={class:"space-y-6"},Aa={class:"space-y-3"},Da={class:"space-y-2"},Fa={class:"flex items-center gap-2 px-3 py-2"},Sa={class:"font-mono text-sm text-foreground"},Ta={key:0,class:"text-xs text-foreground-muted"},Ra={key:0,class:"px-3 pb-2.5 -mt-0.5"},Ma={key:0,class:"text-xs text-danger-fg"},Na={key:1,class:"text-xs text-foreground-muted"},Ia={key:2,class:"flex flex-wrap gap-1"},$a={key:0,class:"text-xs text-foreground-muted"},za={class:"space-y-3"},La=["value"],Ba={key:0,class:"space-y-3"},Oa={class:"space-y-2"},Pa=["value"],qa={key:0,class:"h-2 w-2 rounded-full bg-primary"},Ua={class:"min-w-0"},ja={class:"block text-sm font-medium text-foreground"},Ha={class:"mt-0.5 block text-xs text-foreground-muted leading-snug"},Va={__name:"ProvidersSettings",props:{modelValue:{type:Boolean,default:!1}},emits:["update:modelValue"],setup(e){const u=e,t=He(),n=Eu();async function r(m){await n.ask({title:"Remove provider?",message:"This permanently removes the provider and its encrypted API key.",danger:!0,confirmLabel:"Remove"})&&t.deleteProvider(m)}const o=["openai","anthropic","groq","gemini","ollama","mistral","openrouter","xai","cohere"],a=[{value:"all_writes",label:"Ask before changes (recommended)",hint:"The assistant asks first before it creates, updates, or deletes anything."},{value:"destructive_only",label:"Ask before deletes only",hint:"Routine changes run on their own. Only destructive actions, like deleting, ask first."},{value:"auto",label:"Bypass: allow everything",hint:"Every action runs automatically with no prompts. Fastest, least safe."}],i=R({provider:"openai",label:"",api_key:"",base_url:""}),s=R(!1),c=R(!1),d=R({});function f(m){return d.value[m]||{loading:!1,loaded:!1,models:[],error:""}}ke(()=>u.modelValue,m=>{m&&(t.loadProviders(),t.loadSettings())},{immediate:!0});async function p(m){d.value={...d.value,[m]:{loading:!0,loaded:!1,models:[],error:""}};try{const{data:h}=await X.get(`/ai/providers/${m}/models`);d.value={...d.value,[m]:{loading:!1,loaded:!0,models:h.models||[],error:h.error||""}}}catch(h){d.value={...d.value,[m]:{loading:!1,loaded:!0,models:[],error:h.message}}}}async function b(){if(i.value.provider){s.value=!0;try{await t.saveProvider({provider:i.value.provider,label:i.value.label,api_key:i.value.api_key,base_url:i.value.base_url,enabled:!0}),i.value.api_key=""}finally{s.value=!1}}}async function l(){c.value=!0;try{t.settings.max_tool_iterations=Number(t.settings.max_tool_iterations)||25,await t.saveSettings(t.settings)}finally{c.value=!1}}return(m,h)=>(k(),U(Nn,{"model-value":e.modelValue,title:"Chat settings",size:"md","onUpdate:modelValue":h[7]||(h[7]=g=>m.$emit("update:modelValue",g))},{footer:O(()=>[D(K,{variant:"secondary",onClick:h[6]||(h[6]=g=>m.$emit("update:modelValue",!1))},{default:O(()=>[...h[20]||(h[20]=[P(" Done ",-1)])]),_:1})]),default:O(()=>[x("div",wa,[x("section",Aa,[h[11]||(h[11]=x("div",null,[x("h3",{class:"text-sm font-semibold text-white tracking-tight"}," Providers "),x("p",{class:"text-xs text-foreground-muted mt-0.5"}," Bring your own keys. Keys are encrypted at rest; models are listed live from each provider. Pick the provider + model inside the chat. ")],-1)),x("div",Da,[(k(!0),C(q,null,Y(v(t).providers,g=>(k(),C("div",{key:g.id,class:"rounded-md bg-surface border border-border"},[x("div",Fa,[x("span",Sa,$(g.provider),1),g.label?(k(),C("span",Ta,$(g.label),1)):N("",!0),x("span",{class:z(["text-[10px] uppercase tracking-label rounded px-1.5 py-0.5",g.has_key?"bg-success-tint text-success-fg":"bg-surface-hover text-foreground-muted"])},$(g.has_key?"key set":"no key"),3),h[10]||(h[10]=x("span",{class:"flex-1"},null,-1)),D(K,{size:"xs",variant:"secondary",loading:f(g.id).loading,onClick:E=>p(g.id)},{default:O(()=>[...h[8]||(h[8]=[P(" Models ",-1)])]),_:1},8,["loading","onClick"]),D(K,{size:"xs",variant:"ghost",onClick:E=>r(g.id)},{default:O(()=>[...h[9]||(h[9]=[P(" Remove ",-1)])]),_:1},8,["onClick"])]),f(g.id).loaded?(k(),C("div",Ra,[f(g.id).error?(k(),C("p",Ma,$(f(g.id).error),1)):f(g.id).models.length?(k(),C("div",Ia,[(k(!0),C(q,null,Y(f(g.id).models,E=>(k(),C("span",{key:E.id,class:"text-[11px] font-mono text-foreground-muted bg-surface-hover rounded px-1.5 py-0.5"},$(E.id),1))),128))])):(k(),C("p",Na," No models reported by this endpoint. "))])):N("",!0)]))),128)),v(t).providers.length?N("",!0):(k(),C("p",$a," No providers configured yet. "))])]),x("section",za,[h[14]||(h[14]=x("h3",{class:"text-sm font-semibold text-white tracking-tight"}," Add or update provider ",-1)),h[15]||(h[15]=x("p",{class:"text-xs text-foreground-muted"},[P(" For any OpenAI-compatible endpoint (self-hosted, vLLM, Together, …) choose "),x("span",{class:"font-mono"},"openai"),P(" and set the Base URL. ")],-1)),x("div",null,[h[12]||(h[12]=x("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide"},"Provider",-1)),xu(x("select",{"onUpdate:modelValue":h[0]||(h[0]=g=>i.value.provider=g),class:"mt-1.5 w-full bg-background border border-border rounded-md text-sm px-3 py-2 text-foreground transition-colors duration-200 focus:outline-none focus:ring-1 focus:ring-white focus:border-white"},[(k(),C(q,null,Y(o,g=>x("option",{key:g,value:g},$(g),9,La)),64))],512),[[yn,i.value.provider]])]),D(We,{modelValue:i.value.label,"onUpdate:modelValue":h[1]||(h[1]=g=>i.value.label=g),label:"Label",placeholder:"e.g. personal, work (optional)"},null,8,["modelValue"]),D(We,{modelValue:i.value.base_url,"onUpdate:modelValue":h[2]||(h[2]=g=>i.value.base_url=g),label:"Base URL (optional)",placeholder:"https://api.openai.com/v1 or https://your-host/v1",hint:"For custom / self-hosted endpoints. Either with or without /v1 works."},null,8,["modelValue"]),D(We,{modelValue:i.value.api_key,"onUpdate:modelValue":h[3]||(h[3]=g=>i.value.api_key=g),label:"API key",type:"password",placeholder:"sk-…",hint:"Stored encrypted; never shown again. Leave blank when updating to keep the current key."},null,8,["modelValue"]),D(K,{variant:"primary",loading:s.value,disabled:!i.value.provider,onClick:b},{default:O(()=>[...h[13]||(h[13]=[P(" Save provider ",-1)])]),_:1},8,["loading","disabled"])]),v(t).settings?(k(),C("section",Ba,[h[19]||(h[19]=x("h3",{class:"text-sm font-semibold text-white tracking-tight"}," Defaults ",-1)),x("fieldset",Oa,[h[16]||(h[16]=x("legend",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide"}," Approval policy ",-1)),h[17]||(h[17]=x("p",{class:"text-xs text-foreground-muted leading-snug"}," Reads always run on their own. This controls when the assistant pauses for your OK before it changes anything. ",-1)),(k(),C(q,null,Y(a,g=>x("label",{key:g.value,class:z(["flex cursor-pointer items-start gap-3 rounded-md border px-3 py-2.5 transition-colors focus-within:outline-none focus-within:ring-2 focus-within:ring-inset focus-within:ring-primary",v(t).settings.approval_policy===g.value?"border-primary/50 bg-primary/10":"border-border bg-background hover:bg-surface-hover"])},[xu(x("input",{"onUpdate:modelValue":h[4]||(h[4]=E=>v(t).settings.approval_policy=E),type:"radio",name:"approval-policy",value:g.value,class:"sr-only"},null,8,Pa),[[En,v(t).settings.approval_policy]]),x("span",{class:z(["mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full border transition-colors",v(t).settings.approval_policy===g.value?"border-primary":"border-foreground-muted/50"])},[v(t).settings.approval_policy===g.value?(k(),C("span",qa)):N("",!0)],2),x("span",Ua,[x("span",ja,$(g.label),1),x("span",Ha,$(g.hint),1)])],2)),64))]),D(We,{modelValue:v(t).settings.max_tool_iterations,"onUpdate:modelValue":h[5]||(h[5]=g=>v(t).settings.max_tool_iterations=g),type:"number",label:"Tool steps per reply",hint:"The most tool calls the assistant may chain while answering one message before it stops and responds. Higher allows more complex multi-step tasks; 25 is a sensible default."},null,8,["modelValue"]),D(K,{variant:"primary",loading:c.value,onClick:l},{default:O(()=>[...h[18]||(h[18]=[P(" Save defaults ",-1)])]),_:1},8,["loading"])])):N("",!0)])]),_:1},8,["model-value"]))}},Za={class:"flex h-full bg-background",style:{overflow:"hidden",padding:"0"}},Ga={class:"hidden w-64 shrink-0 border-r border-border md:block"},Wa={class:"flex min-w-0 flex-1 flex-col"},Ka={key:0,class:"flex shrink-0 items-center justify-between gap-3 border-b border-warning-ring bg-warning-tint px-4 py-2.5 text-xs text-warning-fg"},Ja={key:1,class:"scrollable flex flex-1 flex-col items-center justify-center overflow-y-auto px-4 py-6"},Qa={class:"w-full max-w-3xl"},Xa={key:0,class:"flex shrink-0 items-center justify-between gap-3 border-t border-warning-ring bg-warning-tint px-4 py-2.5 text-xs text-warning-fg"},Ya={class:"flex min-w-0 items-center gap-2"},gs={__name:"AI",setup(e){const u=He(),t=Eu();function n({id:b,content:l}){u.editAndResend(b,l)}async function r(b){await t.ask({title:"Delete from here?",message:"This removes this message and everything after it in the conversation. This cannot be undone.",danger:!0,confirmLabel:"Delete"})&&u.deleteMessageFrom(b)}const o=R(!1),a=R(!1),i=R(null);function s(b){i.value?.setText(b)}const c=G(()=>u.conversations.find(b=>b.id===u.activeId)?.title||"Assistant");async function d(){await Promise.all([u.loadSettings(),u.loadProviders(),u.loadConversations()])}Se(d),Cn(()=>{u.loadConversations(),u.loadProviders()}),ke(o,b=>{b||(u.loadProviders(),u.loadSettings())});function f(){const b=document.querySelector('[data-approval="pending"]');b&&(b.scrollIntoView({behavior:"smooth",block:"center"}),b.querySelector("button")?.focus())}function p(b){b.key==="Escape"&&u.streaming&&!document.querySelector('[role="dialog"], [role="menu"]')&&u.stop()}return Se(()=>window.addEventListener("keydown",p)),Cu(()=>window.removeEventListener("keydown",p)),(b,l)=>(k(),C("div",Za,[x("aside",Ga,[D(Ou)]),D(lt,{modelValue:a.value,"onUpdate:modelValue":l[1]||(l[1]=m=>a.value=m),title:"Conversations"},{default:O(()=>[D(Ou,{embedded:"",onSelect:l[0]||(l[0]=m=>a.value=!1)})]),_:1},8,["modelValue"]),x("div",Wa,[D(Wn,{title:c.value,"can-export":!!v(u).timeline.length,onToggleRail:l[2]||(l[2]=m=>a.value=!0),onOpenSettings:l[3]||(l[3]=m=>o.value=!0),onExport:v(u).exportActive},null,8,["title","can-export","onExport"]),v(u).providersLoaded&&!v(u).providers.length?(k(),C("div",Ka,[l[7]||(l[7]=x("span",null,"No AI provider configured. Add a provider API key to start chatting.",-1)),D(K,{size:"xs",variant:"secondary",onClick:l[4]||(l[4]=m=>o.value=!0)},{default:O(()=>[...l[6]||(l[6]=[P(" Configure ",-1)])]),_:1})])):N("",!0),v(u).timeline.length?(k(),C(q,{key:2},[D(ea,{items:v(u).timeline,streaming:v(u).streaming,onApprove:v(u).approveTool,onReject:v(u).rejectTool,onRegenerate:v(u).regenerate,onEdit:n,onDelete:r,onRetry:v(u).retry,onDismiss:v(u).dismissError},null,8,["items","streaming","onApprove","onReject","onRegenerate","onRetry","onDismiss"]),v(u).awaitingApproval?(k(),C("div",Xa,[x("span",Ya,[D(v(In),{class:"h-3.5 w-3.5 shrink-0"}),l[8]||(l[8]=x("span",{class:"truncate"},"The assistant is waiting for your approval.",-1))]),D(K,{size:"xs",variant:"secondary",onClick:f},{default:O(()=>[...l[9]||(l[9]=[P(" Review ",-1)])]),_:1})])):N("",!0),D(at,{streaming:v(u).streaming,disabled:v(u).providersLoaded&&!v(u).providers.length,"model-ready":!!v(u).selectedModel,onSend:v(u).sendMessage,onStop:v(u).stop},null,8,["streaming","disabled","model-ready","onSend","onStop"])],64)):(k(),C("div",Ja,[x("div",Qa,[D(sr,{onPick:s}),D(at,{ref_key:"emptyComposer",ref:i,class:"mt-6",docked:!1,streaming:v(u).streaming,disabled:v(u).providersLoaded&&!v(u).providers.length,"model-ready":!!v(u).selectedModel,onSend:v(u).sendMessage,onStop:v(u).stop},null,8,["streaming","disabled","model-ready","onSend","onStop"])])]))]),D(Va,{modelValue:o.value,"onUpdate:modelValue":l[5]||(l[5]=m=>o.value=m)},null,8,["modelValue"])]))}};export{gs as default}; diff --git a/backend/internal/server/ui_dist/assets/Activity-Dl_m3u_D.js b/backend/internal/server/ui_dist/assets/Activity-Dl_m3u_D.js new file mode 100644 index 0000000..fab5069 --- /dev/null +++ b/backend/internal/server/ui_dist/assets/Activity-Dl_m3u_D.js @@ -0,0 +1 @@ +import{c as ue,j as o,a as l,t as r,f as c,s as ie,q as _,Y as de,o as ce,y as ve,X as xe,Z as me,b as t,d as f,$ as fe,e as pe,v as be,F as k,p as w,g as d,h,_ as C,r as p,aD as he,k as S,n as q}from"./index-CmY58qNN.js";import{E as v}from"./format-CsU4_SPu.js";import{D as _e}from"./Drawer-B7DqDJXO.js";import{_ as V}from"./StatusBadge-MvZdFvHS.js";import{C as ge}from"./chevron-right-9PDhxIr8.js";import"./circle-DeGmlwna.js";import"./clock-CbnKorJ0.js";import"./circle-check-Z_CRCWca.js";const ye=ue("chevron-left",[["path",{d:"m15 18-6-6 6-6",key:"1wnfg3"}]]),F={__name:"SourceTag",props:{source:{type:String,default:""}},setup(A){const D=A,L=_(()=>{switch(D.source){case"web":return"text-indigo-300 border-indigo-900/40";case"api":return"text-sky-300 border-sky-900/40";case"mcp":return"text-violet-300 border-violet-900/40";case"sdk":return"text-teal-300 border-teal-900/40";case"webhook":return"text-amber-300 border-amber-900/40";case"cron":return"text-emerald-300 border-emerald-900/40";case"internal":return"text-foreground-muted border-border";default:return"text-foreground-muted border-border"}});return(O,z)=>(o(),l("span",{class:ie(["inline-flex items-center px-2 py-0.5 rounded text-xs border bg-background font-mono uppercase tracking-wide",L.value])},r(A.source||c(v)),3))}},ke={class:"space-y-6"},we={class:"flex flex-col sm:flex-row sm:items-center gap-2 sm:flex-wrap"},Ce={class:"relative w-full sm:flex-1 sm:min-w-[260px] sm:max-w-[420px]"},Se={class:"flex items-center gap-2 sm:flex-wrap overflow-x-auto sm:overflow-visible scrollable snap-x min-w-0"},Te={key:0,class:"ml-1 opacity-60 tabular-nums"},De={class:"bg-background border border-border rounded-lg overflow-x-auto"},Pe={class:"sm:hidden divide-y divide-border"},$e=["onClick"],Me={class:"flex items-start justify-between gap-2"},qe={class:"min-w-0 flex-1"},Ae={class:"flex items-center gap-2 flex-wrap"},Le={class:"mt-1 text-xs font-mono text-white break-all"},Be={key:0,class:"mt-1 text-[11px] text-foreground-muted break-words"},Ee={class:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-foreground-muted font-mono"},Ie={key:0},Ne={key:1,class:"break-all"},Ve={key:0,class:"px-6 py-12 text-center text-sm text-foreground-muted"},Fe={class:"hidden sm:table w-full text-sm text-left"},Oe={class:"divide-y divide-border"},ze=["onClick"],Re={class:"px-4 py-2.5 font-mono text-xs text-foreground-muted"},Ke={class:"px-4 py-2.5"},Ue={class:"px-4 py-2.5 hidden md:table-cell"},je={class:"text-xs text-white truncate max-w-[200px]"},Je={key:0,class:"text-[10px] text-foreground-muted/70 font-mono truncate"},We={class:"px-4 py-2.5 text-xs font-mono text-foreground-muted hidden sm:table-cell"},Xe={class:"px-4 py-2.5 text-xs font-mono text-white truncate max-w-[440px]"},Ye={class:"px-4 py-2.5 hidden sm:table-cell"},Ze={key:1,class:"text-foreground-muted text-xs"},Ge={class:"px-4 py-2.5 text-xs font-mono text-foreground-muted hidden lg:table-cell"},He={class:"px-4 py-2.5 text-xs text-foreground-muted truncate max-w-[280px] hidden xl:table-cell"},Qe={key:0},et={key:0,class:"flex items-center justify-between text-xs"},tt={class:"text-foreground-muted"},st={class:"flex items-center gap-1"},at={key:0,class:"p-5 space-y-5 text-sm"},rt={class:"grid grid-cols-2 gap-3"},ot={class:"bg-surface border border-border rounded p-3 min-w-0"},lt={class:"text-xs text-white font-mono truncate"},nt={class:"bg-surface border border-border rounded p-3 min-w-0"},ut={class:"bg-surface border border-border rounded p-3 min-w-0"},it={class:"text-sm text-white truncate"},dt={key:0,class:"text-[11px] text-foreground-muted font-mono truncate mt-0.5"},ct={class:"bg-surface border border-border rounded p-3 min-w-0"},vt={class:"flex items-center gap-2"},xt={key:1,class:"text-foreground-muted text-xs"},mt={key:2,class:"text-xs text-foreground-muted font-mono"},ft={class:"bg-surface border border-border rounded p-3 min-w-0"},pt={class:"text-xs text-white font-mono truncate"},bt={class:"bg-surface border border-border rounded p-3 min-w-0"},ht={class:"text-xs text-white font-mono"},_t={class:"bg-surface border border-border rounded p-3 text-xs text-white font-mono whitespace-pre-wrap break-all"},gt={class:"text-foreground break-words"},yt={key:0},kt={class:"bg-surface border border-border rounded p-3 text-xs text-foreground-muted font-mono whitespace-pre-wrap break-all"},wt={key:1},Ct={class:"bg-surface border border-border rounded p-3 text-xs text-foreground font-mono overflow-auto max-h-72 whitespace-pre-wrap break-words"},G=100,H=200,Lt={__name:"Activity",setup(A){const D=de(),L=[{label:"All",value:""},{label:"Web",value:"web"},{label:"API",value:"api"},{label:"MCP",value:"mcp"},{label:"SDK",value:"sdk"},{label:"Webhook",value:"webhook"},{label:"Internal",value:"internal"}],O=[{label:"All",value:""},{label:"Success",value:"ok"},{label:"Errors",value:"err"}],z=[{label:"5m",value:"5m"},{label:"1h",value:"1h"},{label:"24h",value:"24h"},{label:"7d",value:"7d"}],u=p({q:"",source:"",statusBucket:"",range:"24h"}),P=p([]),b=p([]),B=p(!1),n=p(null),R=p(0),E=p(!1),x=p(1),m=p([{since:void 0,until:void 0}]),g=_(()=>x.value===1?[...b.value,...P.value]:P.value),K=_(()=>{const a={};for(const s of g.value)a[s.source]=(a[s.source]||0)+1;return a[""]=g.value.length,a}),$=_(()=>Math.max(m.value.length,x.value)),Q=_(()=>{const a=$.value,s=x.value;return[...new Set([1,a,s-1,s,s+1])].filter(i=>i>=1&&i<=a).sort((i,ne)=>i-ne)}),ee=a=>{switch(a){case"5m":return 5*6e4;case"1h":return 60*6e4;case"24h":return 1440*6e4;case"7d":return 10080*6e4;default:return 0}},te=(a={})=>{const s={limit:G};u.value.source&&(s.source=u.value.source),u.value.statusBucket==="err"&&(s.status_min=400),u.value.q&&(s.q=u.value.q);const e=ee(u.value.range);return e&&(s.since=Date.now()-e),Object.assign(s,a)},M=async a=>{if(a<1||a>m.value.length+1)return;const s=m.value[a-1]?.cursor,e=await he(te(s?{cursor:s}:{}));P.value=e.data?.rows||[];const i=e.data?.next_cursor||0;E.value=i>0,m.value[a-1]||(m.value[a-1]={}),m.value[a-1].cursor=s,i?(m.value[a]||(m.value[a]={}),m.value[a].cursor=i):m.value=m.value.slice(0,a),R.value=(a-1)*G+P.value.length,x.value=a,a>1&&(b.value=[])},y=async()=>{m.value=[{since:void 0,until:void 0}],b.value=[],x.value=1,await M(1)};let T=null;const se=a=>{if(u.value.source&&a.source!==u.value.source||u.value.statusBucket==="err"&&(a.status||0)<400)return!1;if(u.value.q){const s=u.value.q.toLowerCase();if(!(a.path+" "+a.summary+" "+a.actor_label).toLowerCase().includes(s))return!1}return!0},ae=a=>{se(a)&&x.value===1&&(b.value.unshift(a),b.value.length>H&&(b.value=b.value.slice(0,H)))};let U=null;const re=()=>{clearTimeout(U),U=setTimeout(y,250)},j=a=>{n.value=a,B.value=!0},oe=_(()=>n.value?n.value.summary||n.value.method+" "+n.value.path:"Activity"),J=_(()=>{if(!n.value?.metadata)return"";try{return JSON.stringify(JSON.parse(n.value.metadata),null,2)}catch{return n.value.metadata}}),W=a=>a?new Date(a).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1}):v,le=a=>a?new Date(a).toLocaleString():v,I=a=>a==null?v:a<1?"<1ms":a<1e3?a+"ms":(a/1e3).toFixed(2)+"s",N=a=>a?a>=500?"error":a>=400?"failed":a>=200?"success":"pending":"",X=a=>a.id?`db-${a.id}`:`live-${a.ts}-${a.request_id}-${a.path}`,Y=()=>{T||(T=D.subscribe("activity",ae),D.connect())},Z=()=>{T&&(T(),T=null)};return ce(()=>{Y(),y()}),ve(Z),xe(()=>{Y(),y()}),me(Z),(a,s)=>(o(),l("div",ke,[s[20]||(s[20]=t("div",null,[t("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Activity "),t("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"}," Live feed of every API call hitting Orva: UI clicks, REST/SDK, MCP tools, webhook deliveries. ")],-1)),t("div",we,[t("div",Ce,[f(c(fe),{class:"w-3.5 h-3.5 absolute left-2.5 top-1/2 -translate-y-1/2 text-foreground-muted/60 pointer-events-none"}),pe(t("input",{"onUpdate:modelValue":s[0]||(s[0]=e=>u.value.q=e),"aria-label":"Search activity by path, summary, or actor",placeholder:"Search path, summary, actor…",class:"w-full bg-background border border-border rounded-md pl-8 pr-3 py-1.5 text-base sm:text-xs text-foreground placeholder-foreground-muted/60 focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onInput:re},null,544),[[be,u.value.q]])]),t("div",Se,[(o(),l(k,null,w(L,e=>f(C,{key:e.value,variant:"chip",size:"xs",active:u.value.source===e.value,class:"shrink-0 snap-start",onClick:i=>{u.value.source=e.value,y()}},{default:h(()=>[S(r(e.label)+" ",1),K.value[e.value]!=null&&e.value!==""?(o(),l("span",Te,r(K.value[e.value]),1)):d("",!0)]),_:2},1032,["active","onClick"])),64)),s[4]||(s[4]=t("span",{class:"text-foreground-muted/40 shrink-0"},"·",-1)),(o(),l(k,null,w(O,e=>f(C,{key:e.value,variant:"chip",size:"xs",active:u.value.statusBucket===e.value,class:"shrink-0 snap-start",onClick:i=>{u.value.statusBucket=e.value,y()}},{default:h(()=>[S(r(e.label),1)]),_:2},1032,["active","onClick"])),64)),s[5]||(s[5]=t("span",{class:"text-foreground-muted/40 shrink-0"},"·",-1)),(o(),l(k,null,w(z,e=>f(C,{key:e.value,variant:"chip",size:"xs",active:u.value.range===e.value,class:"shrink-0 snap-start",onClick:i=>{u.value.range=e.value,y()}},{default:h(()=>[S(r(e.label),1)]),_:2},1032,["active","onClick"])),64))])]),t("div",De,[t("ul",Pe,[(o(!0),l(k,null,w(g.value,e=>(o(),l("li",{key:X(e),class:"px-4 py-3 cursor-pointer hover:bg-surface-hover transition-colors",onClick:i=>j(e)},[t("div",Me,[t("div",qe,[t("div",Ae,[f(F,{source:e.source},null,8,["source"]),e.status?(o(),q(V,{key:0,status:N(e.status)},null,8,["status"])):d("",!0)]),t("div",Le,r(e.method?e.method+" ":"")+r(e.path||c(v)),1),e.summary?(o(),l("div",Be,r(e.summary),1)):d("",!0),t("div",Ee,[t("span",null,r(W(e.ts)),1),e.duration_ms!=null?(o(),l("span",Ie,r(I(e.duration_ms)),1)):d("",!0),e.actor_label||e.actor_id?(o(),l("span",Ne,r(e.actor_label||e.actor_id),1)):d("",!0)])])])],8,$e))),128)),g.value.length?d("",!0):(o(),l("li",Ve," No activity yet. Drive any action (open the dashboard, call a function, fire an MCP tool) and rows will land here. "))]),t("table",Fe,[s[7]||(s[7]=t("thead",{class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},[t("tr",null,[t("th",{class:"px-4 py-3 w-32"},"Time"),t("th",{class:"px-4 py-3 w-24"},"Source"),t("th",{class:"px-4 py-3 w-40 hidden md:table-cell"},"Actor"),t("th",{class:"px-4 py-3 w-20 hidden sm:table-cell"},"Method"),t("th",{class:"px-4 py-3"},"Path / Tool"),t("th",{class:"px-4 py-3 w-16 hidden sm:table-cell"},"Status"),t("th",{class:"px-4 py-3 w-20 hidden lg:table-cell"},"Duration"),t("th",{class:"px-4 py-3 hidden xl:table-cell"},"Summary")])],-1)),t("tbody",Oe,[(o(!0),l(k,null,w(g.value,e=>(o(),l("tr",{key:X(e),class:"hover:bg-surface-hover cursor-pointer transition-colors",onClick:i=>j(e)},[t("td",Re,r(W(e.ts)),1),t("td",Ke,[f(F,{source:e.source},null,8,["source"])]),t("td",Ue,[t("div",je,r(e.actor_label||e.actor_id||c(v)),1),e.actor_label&&e.actor_id&&e.actor_label!==e.actor_id?(o(),l("div",Je,r(e.actor_id),1)):d("",!0)]),t("td",We,r(e.method||c(v)),1),t("td",Xe,r(e.path||c(v)),1),t("td",Ye,[e.status?(o(),q(V,{key:0,status:N(e.status)},null,8,["status"])):(o(),l("span",Ze,r(c(v)),1))]),t("td",Ge,r(I(e.duration_ms)),1),t("td",He,r(e.summary),1)],8,ze))),128)),g.value.length?d("",!0):(o(),l("tr",Qe,[...s[6]||(s[6]=[t("td",{colspan:"8",class:"px-4 py-12 text-center text-foreground-muted text-sm"}," No activity yet. Drive any action (open the dashboard, call a function, fire an MCP tool) and rows will land here. ",-1)])]))])])]),$.value>1?(o(),l("div",et,[t("div",tt," Page "+r(x.value)+" of "+r($.value)+" · "+r(R.value)+r(E.value?"+":"")+" rows ",1),t("div",st,[f(C,{variant:"secondary",size:"xs",disabled:x.value<=1,onClick:s[1]||(s[1]=e=>M(x.value-1))},{default:h(()=>[f(c(ye),{class:"w-3.5 h-3.5"}),s[8]||(s[8]=S(" Prev ",-1))]),_:1},8,["disabled"]),(o(!0),l(k,null,w(Q.value,e=>(o(),q(C,{key:e,variant:e===x.value?"primary":"secondary",size:"xs",onClick:i=>M(e)},{default:h(()=>[S(r(e),1)]),_:2},1032,["variant","onClick"]))),128)),f(C,{variant:"secondary",size:"xs",disabled:x.value>=$.value&&!E.value,onClick:s[2]||(s[2]=e=>M(x.value+1))},{default:h(()=>[s[9]||(s[9]=S(" Next ",-1)),f(c(ge),{class:"w-3.5 h-3.5"})]),_:1},8,["disabled"])])])):d("",!0),f(_e,{modelValue:B.value,"onUpdate:modelValue":s[3]||(s[3]=e=>B.value=e),title:oe.value,width:"640px"},{default:h(()=>[n.value?(o(),l("div",at,[t("div",rt,[t("div",ot,[s[10]||(s[10]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Time",-1)),t("div",lt,r(le(n.value.ts)),1)]),t("div",nt,[s[11]||(s[11]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Source",-1)),f(F,{source:n.value.source},null,8,["source"])]),t("div",ut,[s[12]||(s[12]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Actor",-1)),t("div",it,r(n.value.actor_label||c(v)),1),n.value.actor_id?(o(),l("div",dt,r(n.value.actor_id),1)):d("",!0)]),t("div",ct,[s[13]||(s[13]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Status",-1)),t("div",vt,[n.value.status?(o(),q(V,{key:0,status:N(n.value.status)},null,8,["status"])):(o(),l("span",xt,r(c(v)),1)),n.value.status?(o(),l("span",mt,"HTTP "+r(n.value.status),1)):d("",!0)])]),t("div",ft,[s[14]||(s[14]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Method",-1)),t("div",pt,r(n.value.method||c(v)),1)]),t("div",bt,[s[15]||(s[15]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Duration",-1)),t("div",ht,r(I(n.value.duration_ms)),1)])]),t("div",null,[s[16]||(s[16]=t("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},"Path / Tool",-1)),t("pre",_t,r(n.value.path||c(v)),1)]),t("div",null,[s[17]||(s[17]=t("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},"Summary",-1)),t("div",gt,r(n.value.summary||c(v)),1)]),n.value.request_id?(o(),l("div",yt,[s[18]||(s[18]=t("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},"Request ID",-1)),t("pre",kt,r(n.value.request_id),1)])):d("",!0),J.value?(o(),l("div",wt,[s[19]||(s[19]=t("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},"Metadata",-1)),t("pre",Ct,r(J.value),1)])):d("",!0)])):d("",!0)]),_:1},8,["modelValue","title"])]))}};export{Lt as default}; diff --git a/backend/internal/server/ui_dist/assets/Activity-DqwayTo3.js b/backend/internal/server/ui_dist/assets/Activity-DqwayTo3.js deleted file mode 100644 index 60f9175..0000000 --- a/backend/internal/server/ui_dist/assets/Activity-DqwayTo3.js +++ /dev/null @@ -1 +0,0 @@ -import{c as ue,j as l,a as u,t as a,f as i,s as de,q as g,D as ie,o as ce,y as ve,E as xe,G as me,b as t,d as m,S as fe,e as be,v as pe,F as S,p as C,g as f,h,_ as y,r as b,ax as he,k,n as E}from"./index-WXhXpu06.js";import{E as c}from"./format-CsU4_SPu.js";import{D as ge}from"./Drawer-DFsQitq5.js";import{_ as G}from"./StatusBadge-B3a6roHm.js";import{C as _e}from"./chevron-right-DYfLurIz.js";const ye=ue("chevron-left",[["path",{d:"m15 18-6-6 6-6",key:"1wnfg3"}]]),J={__name:"SourceTag",props:{source:{type:String,default:""}},setup(M){const T=M,$=g(()=>{switch(T.source){case"web":return"text-indigo-300 border-indigo-900/40";case"api":return"text-sky-300 border-sky-900/40";case"mcp":return"text-violet-300 border-violet-900/40";case"sdk":return"text-teal-300 border-teal-900/40";case"webhook":return"text-amber-300 border-amber-900/40";case"cron":return"text-emerald-300 border-emerald-900/40";case"internal":return"text-foreground-muted border-border";default:return"text-foreground-muted border-border"}});return(I,V)=>(l(),u("span",{class:de(["inline-flex items-center px-2 py-0.5 rounded text-xs border bg-background font-mono uppercase tracking-wide",$.value])},a(M.source||i(c)),3))}},ke={class:"space-y-6"},we={class:"flex flex-col sm:flex-row sm:items-center gap-2 sm:flex-wrap"},Se={class:"relative w-full sm:flex-1 sm:min-w-[260px] sm:max-w-[420px]"},Ce={class:"flex items-center gap-2 sm:flex-wrap overflow-x-auto sm:overflow-visible scrollable snap-x min-w-0"},Te={key:0,class:"ml-1 opacity-60 tabular-nums"},De={class:"bg-background border border-border rounded-lg overflow-x-auto"},Pe={class:"w-full text-sm text-left"},qe={class:"divide-y divide-border"},Ae=["onClick"],Me={class:"px-4 py-2.5 font-mono text-xs text-foreground-muted"},$e={class:"px-4 py-2.5"},Le={class:"px-4 py-2.5 hidden md:table-cell"},Be={class:"text-xs text-white truncate max-w-[200px]"},Ee={key:0,class:"text-[10px] text-foreground-muted/70 font-mono truncate"},Ie={class:"px-4 py-2.5 text-xs font-mono text-foreground-muted hidden sm:table-cell"},Ve={class:"px-4 py-2.5 text-xs font-mono text-white truncate max-w-[440px]"},Fe={class:"px-4 py-2.5 hidden sm:table-cell"},Ne={key:1,class:"text-foreground-muted text-xs"},Oe={class:"px-4 py-2.5 text-xs font-mono text-foreground-muted hidden lg:table-cell"},ze={class:"px-4 py-2.5 text-xs text-foreground-muted truncate max-w-[280px] hidden xl:table-cell"},Re={key:0},Ke={key:0,class:"flex items-center justify-between text-xs"},Ue={class:"text-foreground-muted"},je={class:"flex items-center gap-1"},Ge={key:0,class:"p-5 space-y-5 text-sm"},Je={class:"grid grid-cols-2 gap-3"},We={class:"bg-surface border border-border rounded p-3 min-w-0"},He={class:"text-xs text-white font-mono truncate"},Xe={class:"bg-surface border border-border rounded p-3 min-w-0"},Ye={class:"bg-surface border border-border rounded p-3 min-w-0"},Ze={class:"text-sm text-white truncate"},Qe={key:0,class:"text-[11px] text-foreground-muted font-mono truncate mt-0.5"},et={class:"bg-surface border border-border rounded p-3 min-w-0"},tt={class:"flex items-center gap-2"},st={key:1,class:"text-foreground-muted text-xs"},rt={key:2,class:"text-xs text-foreground-muted font-mono"},at={class:"bg-surface border border-border rounded p-3 min-w-0"},ot={class:"text-xs text-white font-mono truncate"},lt={class:"bg-surface border border-border rounded p-3 min-w-0"},nt={class:"text-xs text-white font-mono"},ut={class:"bg-surface border border-border rounded p-3 text-xs text-white font-mono whitespace-pre-wrap break-all"},dt={class:"text-foreground break-words"},it={key:0},ct={class:"bg-surface border border-border rounded p-3 text-xs text-foreground-muted font-mono whitespace-pre-wrap break-all"},vt={key:1},xt={class:"bg-surface border border-border rounded p-3 text-xs text-foreground font-mono overflow-auto max-h-72 whitespace-pre-wrap break-words"},W=100,H=200,gt={__name:"Activity",setup(M){const T=ie(),$=[{label:"All",value:""},{label:"Web",value:"web"},{label:"API",value:"api"},{label:"MCP",value:"mcp"},{label:"SDK",value:"sdk"},{label:"Webhook",value:"webhook"},{label:"Internal",value:"internal"}],I=[{label:"All",value:""},{label:"Success",value:"ok"},{label:"Errors",value:"err"}],V=[{label:"5m",value:"5m"},{label:"1h",value:"1h"},{label:"24h",value:"24h"},{label:"7d",value:"7d"}],n=b({q:"",source:"",statusBucket:"",range:"24h"}),D=b([]),p=b([]),L=b(!1),o=b(null),F=b(0),B=b(!1),v=b(1),x=b([{since:void 0,until:void 0}]),P=g(()=>v.value===1?[...p.value,...D.value]:D.value),N=g(()=>{const s={};for(const e of P.value)s[e.source]=(s[e.source]||0)+1;return s[""]=P.value.length,s}),q=g(()=>Math.max(x.value.length,v.value)),X=g(()=>{const s=q.value,e=v.value;return[...new Set([1,s,e-1,e,e+1])].filter(d=>d>=1&&d<=s).sort((d,ne)=>d-ne)}),Y=s=>{switch(s){case"5m":return 5*6e4;case"1h":return 60*6e4;case"24h":return 1440*6e4;case"7d":return 10080*6e4;default:return 0}},Z=(s={})=>{const e={limit:W};n.value.source&&(e.source=n.value.source),n.value.statusBucket==="err"&&(e.status_min=400),n.value.q&&(e.q=n.value.q);const r=Y(n.value.range);return r&&(e.since=Date.now()-r),Object.assign(e,s)},A=async s=>{if(s<1||s>x.value.length+1)return;const e=x.value[s-1]?.cursor,r=await he(Z(e?{cursor:e}:{}));D.value=r.data?.rows||[];const d=r.data?.next_cursor||0;B.value=d>0,x.value[s-1]||(x.value[s-1]={}),x.value[s-1].cursor=e,d?(x.value[s]||(x.value[s]={}),x.value[s].cursor=d):x.value=x.value.slice(0,s),F.value=(s-1)*W+D.value.length,v.value=s,s>1&&(p.value=[])},_=async()=>{x.value=[{since:void 0,until:void 0}],p.value=[],v.value=1,await A(1)};let w=null;const Q=s=>{if(n.value.source&&s.source!==n.value.source||n.value.statusBucket==="err"&&(s.status||0)<400)return!1;if(n.value.q){const e=n.value.q.toLowerCase();if(!(s.path+" "+s.summary+" "+s.actor_label).toLowerCase().includes(e))return!1}return!0},ee=s=>{Q(s)&&v.value===1&&(p.value.unshift(s),p.value.length>H&&(p.value=p.value.slice(0,H)))};let O=null;const te=()=>{clearTimeout(O),O=setTimeout(_,250)},se=s=>{o.value=s,L.value=!0},re=g(()=>o.value?o.value.summary||o.value.method+" "+o.value.path:"Activity"),z=g(()=>{if(!o.value?.metadata)return"";try{return JSON.stringify(JSON.parse(o.value.metadata),null,2)}catch{return o.value.metadata}}),ae=s=>s?new Date(s).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1}):c,oe=s=>s?new Date(s).toLocaleString():c,R=s=>s==null?c:s<1?"<1ms":s<1e3?s+"ms":(s/1e3).toFixed(2)+"s",K=s=>s?s>=500?"error":s>=400?"failed":s>=200?"success":"pending":"",le=s=>s.id?`db-${s.id}`:`live-${s.ts}-${s.request_id}-${s.path}`,U=()=>{w||(w=T.subscribe("activity",ee),T.connect())},j=()=>{w&&(w(),w=null)};return ce(()=>{U(),_()}),ve(j),xe(()=>{U(),_()}),me(j),(s,e)=>(l(),u("div",ke,[e[20]||(e[20]=t("div",null,[t("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Activity "),t("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"}," Live feed of every API call hitting Orva: UI clicks, REST/SDK, MCP tools, webhook deliveries. ")],-1)),t("div",we,[t("div",Se,[m(i(fe),{class:"w-3.5 h-3.5 absolute left-2.5 top-1/2 -translate-y-1/2 text-foreground-muted/60 pointer-events-none"}),be(t("input",{"onUpdate:modelValue":e[0]||(e[0]=r=>n.value.q=r),placeholder:"Search path, summary, actor…",class:"w-full bg-background border border-border rounded-md pl-8 pr-3 py-1.5 text-base sm:text-xs text-foreground placeholder-foreground-muted/60 focus:outline-none focus:border-white",onInput:te},null,544),[[pe,n.value.q]])]),t("div",Ce,[(l(),u(S,null,C($,r=>m(y,{key:r.value,variant:"chip",size:"xs",active:n.value.source===r.value,class:"shrink-0 snap-start",onClick:d=>{n.value.source=r.value,_()}},{default:h(()=>[k(a(r.label)+" ",1),N.value[r.value]!=null&&r.value!==""?(l(),u("span",Te,a(N.value[r.value]),1)):f("",!0)]),_:2},1032,["active","onClick"])),64)),e[4]||(e[4]=t("span",{class:"text-foreground-muted/40 shrink-0"},"·",-1)),(l(),u(S,null,C(I,r=>m(y,{key:r.value,variant:"chip",size:"xs",active:n.value.statusBucket===r.value,class:"shrink-0 snap-start",onClick:d=>{n.value.statusBucket=r.value,_()}},{default:h(()=>[k(a(r.label),1)]),_:2},1032,["active","onClick"])),64)),e[5]||(e[5]=t("span",{class:"text-foreground-muted/40 shrink-0"},"·",-1)),(l(),u(S,null,C(V,r=>m(y,{key:r.value,variant:"chip",size:"xs",active:n.value.range===r.value,class:"shrink-0 snap-start",onClick:d=>{n.value.range=r.value,_()}},{default:h(()=>[k(a(r.label),1)]),_:2},1032,["active","onClick"])),64))])]),t("div",De,[t("table",Pe,[e[7]||(e[7]=t("thead",{class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},[t("tr",null,[t("th",{class:"px-4 py-3 w-32"},"Time"),t("th",{class:"px-4 py-3 w-24"},"Source"),t("th",{class:"px-4 py-3 w-40 hidden md:table-cell"},"Actor"),t("th",{class:"px-4 py-3 w-20 hidden sm:table-cell"},"Method"),t("th",{class:"px-4 py-3"},"Path / Tool"),t("th",{class:"px-4 py-3 w-16 hidden sm:table-cell"},"Status"),t("th",{class:"px-4 py-3 w-20 hidden lg:table-cell"},"Duration"),t("th",{class:"px-4 py-3 hidden xl:table-cell"},"Summary")])],-1)),t("tbody",qe,[(l(!0),u(S,null,C(P.value,r=>(l(),u("tr",{key:le(r),class:"hover:bg-surface/40 cursor-pointer transition-colors",onClick:d=>se(r)},[t("td",Me,a(ae(r.ts)),1),t("td",$e,[m(J,{source:r.source},null,8,["source"])]),t("td",Le,[t("div",Be,a(r.actor_label||r.actor_id||i(c)),1),r.actor_label&&r.actor_id&&r.actor_label!==r.actor_id?(l(),u("div",Ee,a(r.actor_id),1)):f("",!0)]),t("td",Ie,a(r.method||i(c)),1),t("td",Ve,a(r.path||i(c)),1),t("td",Fe,[r.status?(l(),E(G,{key:0,status:K(r.status)},null,8,["status"])):(l(),u("span",Ne,a(i(c)),1))]),t("td",Oe,a(R(r.duration_ms)),1),t("td",ze,a(r.summary),1)],8,Ae))),128)),P.value.length?f("",!0):(l(),u("tr",Re,[...e[6]||(e[6]=[t("td",{colspan:"8",class:"px-4 py-12 text-center text-foreground-muted text-sm"}," No activity yet. Drive any action (open the dashboard, call a function, fire an MCP tool) and rows will land here. ",-1)])]))])])]),q.value>1?(l(),u("div",Ke,[t("div",Ue," Page "+a(v.value)+" of "+a(q.value)+" · "+a(F.value)+a(B.value?"+":"")+" rows ",1),t("div",je,[m(y,{variant:"secondary",size:"xs",disabled:v.value<=1,onClick:e[1]||(e[1]=r=>A(v.value-1))},{default:h(()=>[m(i(ye),{class:"w-3.5 h-3.5"}),e[8]||(e[8]=k(" Prev ",-1))]),_:1},8,["disabled"]),(l(!0),u(S,null,C(X.value,r=>(l(),E(y,{key:r,variant:r===v.value?"primary":"secondary",size:"xs",onClick:d=>A(r)},{default:h(()=>[k(a(r),1)]),_:2},1032,["variant","onClick"]))),128)),m(y,{variant:"secondary",size:"xs",disabled:v.value>=q.value&&!B.value,onClick:e[2]||(e[2]=r=>A(v.value+1))},{default:h(()=>[e[9]||(e[9]=k(" Next ",-1)),m(i(_e),{class:"w-3.5 h-3.5"})]),_:1},8,["disabled"])])])):f("",!0),m(ge,{modelValue:L.value,"onUpdate:modelValue":e[3]||(e[3]=r=>L.value=r),title:re.value,width:"640px"},{default:h(()=>[o.value?(l(),u("div",Ge,[t("div",Je,[t("div",We,[e[10]||(e[10]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Time",-1)),t("div",He,a(oe(o.value.ts)),1)]),t("div",Xe,[e[11]||(e[11]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Source",-1)),m(J,{source:o.value.source},null,8,["source"])]),t("div",Ye,[e[12]||(e[12]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Actor",-1)),t("div",Ze,a(o.value.actor_label||i(c)),1),o.value.actor_id?(l(),u("div",Qe,a(o.value.actor_id),1)):f("",!0)]),t("div",et,[e[13]||(e[13]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Status",-1)),t("div",tt,[o.value.status?(l(),E(G,{key:0,status:K(o.value.status)},null,8,["status"])):(l(),u("span",st,a(i(c)),1)),o.value.status?(l(),u("span",rt,"HTTP "+a(o.value.status),1)):f("",!0)])]),t("div",at,[e[14]||(e[14]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Method",-1)),t("div",ot,a(o.value.method||i(c)),1)]),t("div",lt,[e[15]||(e[15]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Duration",-1)),t("div",nt,a(R(o.value.duration_ms)),1)])]),t("div",null,[e[16]||(e[16]=t("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},"Path / Tool",-1)),t("pre",ut,a(o.value.path||i(c)),1)]),t("div",null,[e[17]||(e[17]=t("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},"Summary",-1)),t("div",dt,a(o.value.summary||i(c)),1)]),o.value.request_id?(l(),u("div",it,[e[18]||(e[18]=t("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},"Request ID",-1)),t("pre",ct,a(o.value.request_id),1)])):f("",!0),z.value?(l(),u("div",vt,[e[19]||(e[19]=t("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},"Metadata",-1)),t("pre",xt,a(z.value),1)])):f("",!0)])):f("",!0)]),_:1},8,["modelValue","title"])]))}};export{gt as default}; diff --git a/backend/internal/server/ui_dist/assets/ApiKeys-Bk7arMia.js b/backend/internal/server/ui_dist/assets/ApiKeys-Bk7arMia.js deleted file mode 100644 index 60648b7..0000000 --- a/backend/internal/server/ui_dist/assets/ApiKeys-Bk7arMia.js +++ /dev/null @@ -1,3 +0,0 @@ -import{C as V,o as $,a,b as e,d as c,h,_ as w,f as n,a3 as j,t as l,n as C,k as i,g as y,e as D,v as B,R as F,F as K,p as N,aL as R,r as p,j as o,aM as G,aN as H}from"./index-WXhXpu06.js";import{E as X}from"./format-CsU4_SPu.js";import{_ as I}from"./IconButton-CclydhPm.js";import{c as Y}from"./clipboard-CmSw2rR-.js";import{f as x,i as A}from"./time-Cfu9zNbw.js";import{C as q}from"./check-CuO1EVch.js";import{C as z}from"./copy-HWkx-vox.js";import{K as J}from"./key-round-BMBM5azh.js";import{T}from"./trash-2-dFLn8UYZ.js";const O={class:"space-y-6"},Q={class:"flex items-start justify-between gap-4"},W={key:0,class:"bg-background border border-amber-700/40 rounded-lg p-4 space-y-2"},Z={class:"flex items-start justify-between gap-3"},ee={class:"flex items-center gap-2"},te={class:"flex-1 font-mono text-sm text-white break-all bg-surface px-3 py-2 rounded border border-border"},se={key:1,class:"bg-background border border-border rounded-lg p-5 space-y-4"},oe={class:"grid grid-cols-1 md:grid-cols-2 gap-3"},ae={class:"flex gap-2 pt-1"},re={class:"bg-background border border-border rounded-lg overflow-x-auto"},ne={class:"sm:hidden divide-y divide-border"},le={class:"flex items-start justify-between gap-2"},de={class:"min-w-0 flex-1"},ie={class:"flex items-center gap-2 flex-wrap"},ce={class:"font-medium text-white truncate"},ue={key:0,class:"text-[11px] font-mono text-foreground-muted bg-surface px-1.5 py-0.5 rounded"},pe={class:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-foreground-muted"},xe={key:0},me={key:1,class:"text-amber-400/80"},fe={key:2},ye={key:3,class:"text-red-400"},ve={key:4},ge={key:0,class:"px-6 py-8 text-center text-sm text-foreground-muted"},be={class:"hidden sm:table w-full text-sm text-left"},_e={class:"divide-y divide-border"},he={class:"px-6 py-4 text-white font-medium"},we={class:"px-6 py-4 text-foreground-muted font-mono text-xs hidden sm:table-cell"},ke={class:"px-6 py-4 text-foreground-muted hidden xl:table-cell"},Ce={class:"px-6 py-4 hidden md:table-cell"},De={key:0,class:"text-foreground-muted"},Ke={key:1,class:"text-amber-400/70 text-xs"},Ne={class:"px-6 py-4 hidden lg:table-cell"},Ie={key:0,class:"text-foreground-muted"},Ae={key:1,class:"text-red-400 text-xs"},Te={key:2,class:"text-foreground-muted"},Pe={class:"px-6 py-4 text-right"},Ee={key:0},Ge={__name:"ApiKeys",setup(Se){const v=V(),m=p([]),u=p(""),f=p(!1),g=p(!1),b=p(!1),d=p({name:"",expiresInDays:0}),_=async()=>{const r=await R();m.value=r.data.keys||[]},P=()=>{d.value={name:"",expiresInDays:0},g.value=!0},E=()=>{g.value=!1,d.value={name:"",expiresInDays:0}},S=async()=>{b.value=!0;try{const r={name:d.value.name.trim()};d.value.expiresInDays>0&&(r.expires_in_days=d.value.expiresInDays);const t=await G(r);u.value=t.data.key,f.value=!1,g.value=!1,await _()}catch(r){console.error(r),v.notify({title:"Failed to create key",message:r?.response?.data?.error?.message||"Unknown error",danger:!0})}finally{b.value=!1}},U=async()=>{await Y(u.value)?(f.value=!0,setTimeout(()=>{f.value=!1},1500)):v.notify({title:"Copy failed",message:`Could not copy to clipboard. Select the key manually: - -`+u.value})},k=async r=>{if(await v.ask({title:"Delete API key?",message:`"${r.name||r.id}" will stop working immediately. This cannot be undone.`,confirmLabel:"Delete",danger:!0}))try{await H(r.id),await _()}catch(s){console.error(s),v.notify({title:"Failed to delete key",message:s?.response?.data?.error?.message||"Unknown error",danger:!0})}},L=r=>new Date(r).toLocaleString();return $(_),(r,t)=>(o(),a("div",O,[e("div",Q,[t[4]||(t[4]=e("div",null,[e("h1",{class:"text-xl font-semibold text-white tracking-tight"}," API Keys "),e("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"}," Long-lived bearer tokens that authorise REST and MCP calls from CI, scripts, and external services. Plaintext is shown once at creation; the server keeps only a SHA-256 hash. ")],-1)),c(w,{onClick:P},{default:h(()=>[c(n(J),{class:"w-4 h-4"}),t[3]||(t[3]=i(" New Key ",-1))]),_:1})]),u.value?(o(),a("div",W,[e("div",Z,[t[5]||(t[5]=e("div",null,[e("h2",{class:"text-xs font-bold text-amber-300 uppercase tracking-wider"}," Copy this key now "),e("div",{class:"text-xs text-foreground-muted mt-0.5"}," It will not be shown again. Anyone with this key can invoke your functions. ")],-1)),e("button",{class:"text-foreground-muted hover:text-white",title:"Dismiss",onClick:t[0]||(t[0]=s=>u.value="")},[c(n(j),{class:"w-4 h-4"})])]),e("div",ee,[e("code",te,l(u.value),1),e("button",{class:"px-3 py-2 rounded-md border border-border bg-surface-hover hover:bg-surface text-foreground-muted hover:text-white transition-colors flex items-center gap-1.5 text-xs",onClick:U},[f.value?(o(),C(n(q),{key:0,class:"w-3.5 h-3.5 text-success"})):(o(),C(n(z),{key:1,class:"w-3.5 h-3.5"})),i(" "+l(f.value?"Copied":"Copy"),1)])])])):y("",!0),g.value?(o(),a("div",se,[t[11]||(t[11]=e("div",{class:"text-sm font-semibold text-white"}," New API Key ",-1)),e("div",oe,[e("div",null,[t[6]||(t[6]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"Name",-1)),D(e("input",{"onUpdate:modelValue":t[1]||(t[1]=s=>d.value.name=s),placeholder:"e.g. ci-deployer",class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:border-white"},null,512),[[B,d.value.name]])]),e("div",null,[t[8]||(t[8]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"Expires in",-1)),D(e("select",{"onUpdate:modelValue":t[2]||(t[2]=s=>d.value.expiresInDays=s),class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:border-white"},[...t[7]||(t[7]=[e("option",{value:0}," Never ",-1),e("option",{value:1}," 1 day ",-1),e("option",{value:7}," 7 days ",-1),e("option",{value:30}," 30 days ",-1),e("option",{value:90}," 90 days ",-1),e("option",{value:365}," 1 year ",-1)])],512),[[F,d.value.expiresInDays]])])]),e("div",ae,[c(w,{disabled:!d.value.name.trim()||b.value,loading:b.value,onClick:S},{default:h(()=>[...t[9]||(t[9]=[i(" Generate Key ",-1)])]),_:1},8,["disabled","loading"]),c(w,{variant:"secondary",onClick:E},{default:h(()=>[...t[10]||(t[10]=[i(" Cancel ",-1)])]),_:1})])])):y("",!0),e("div",re,[e("ul",ne,[(o(!0),a(K,null,N(m.value,s=>(o(),a("li",{key:s.id,class:"px-4 py-3"},[e("div",le,[e("div",de,[e("div",ie,[e("span",ce,l(s.name||"Unnamed"),1),s.prefix?(o(),a("code",ue,l(s.prefix)+"…",1)):y("",!0)]),e("div",pe,[s.last_used_at?(o(),a("span",xe,"used "+l(n(x)(s.last_used_at)),1)):(o(),a("span",me,"never used")),s.expires_at?n(A)(s.expires_at)?(o(),a("span",ye,"expired "+l(n(x)(s.expires_at)),1)):(o(),a("span",ve,"expires "+l(n(x)(s.expires_at)),1)):(o(),a("span",fe,"no expiry"))])]),c(I,{icon:n(T),variant:"danger",title:"Delete key",onClick:M=>k(s)},null,8,["icon","onClick"])])]))),128)),m.value.length===0?(o(),a("li",ge,[...t[12]||(t[12]=[i(" No API keys yet. Tap ",-1),e("span",{class:"text-white"},"New Key",-1),i(" to generate one. ",-1)])])):y("",!0)]),e("table",be,[t[14]||(t[14]=e("thead",{class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},[e("tr",null,[e("th",{class:"px-6 py-3 font-medium"}," Name "),e("th",{class:"px-6 py-3 font-medium hidden sm:table-cell"}," Prefix "),e("th",{class:"px-6 py-3 font-medium hidden xl:table-cell"}," Created "),e("th",{class:"px-6 py-3 font-medium hidden md:table-cell"}," Last Used "),e("th",{class:"px-6 py-3 font-medium hidden lg:table-cell"}," Expires "),e("th",{class:"px-6 py-3 font-medium text-right"}," Actions ")])],-1)),e("tbody",_e,[(o(!0),a(K,null,N(m.value,s=>(o(),a("tr",{key:s.id,class:"hover:bg-surface/50 transition-colors"},[e("td",he,l(s.name||"Unnamed"),1),e("td",we,l(s.prefix?s.prefix+"…":n(X)),1),e("td",ke,l(L(s.created_at)),1),e("td",Ce,[s.last_used_at?(o(),a("span",De,l(n(x)(s.last_used_at)),1)):(o(),a("span",Ke,"Never used"))]),e("td",Ne,[s.expires_at?n(A)(s.expires_at)?(o(),a("span",Ae,"Expired "+l(n(x)(s.expires_at)),1)):(o(),a("span",Te,l(n(x)(s.expires_at)),1)):(o(),a("span",Ie,"Never"))]),e("td",Pe,[c(I,{icon:n(T),variant:"danger",title:"Delete key",onClick:M=>k(s)},null,8,["icon","onClick"])])]))),128)),m.value.length===0?(o(),a("tr",Ee,[...t[13]||(t[13]=[e("td",{colspan:"6",class:"px-6 py-8 text-center text-foreground-muted"},[i(" No API keys yet. Click "),e("span",{class:"text-white"},"New Key"),i(" to generate one. ")],-1)])])):y("",!0)])])])]))}};export{Ge as default}; diff --git a/backend/internal/server/ui_dist/assets/ApiKeys-DmvlOqPv.js b/backend/internal/server/ui_dist/assets/ApiKeys-DmvlOqPv.js new file mode 100644 index 0000000..4200ff6 --- /dev/null +++ b/backend/internal/server/ui_dist/assets/ApiKeys-DmvlOqPv.js @@ -0,0 +1,3 @@ +import{G as L,o as M,a,b as e,d as c,h,_ as w,f as n,ac as j,t as l,n as C,k as i,g as y,e as D,v as B,V as F,F as K,p as N,aP as R,r as p,j as o,aQ as G,aR as H}from"./index-CmY58qNN.js";import{E as Q}from"./format-CsU4_SPu.js";import{_ as I}from"./IconButton-i2ilFBbf.js";import{c as X}from"./clipboard-CmSw2rR-.js";import{f as x,i as A}from"./time-Cfu9zNbw.js";import{C as Y}from"./check-z8qyH5P-.js";import{C as q}from"./copy-BzujsGZw.js";import{K as z}from"./key-round-ByFVPOPP.js";import{T as P}from"./trash-2-BJzRpJ7Y.js";const J={class:"space-y-6"},O={class:"flex items-start justify-between gap-4"},W={key:0,class:"bg-background border border-amber-700/40 rounded-lg p-4 space-y-2"},Z={class:"flex items-start justify-between gap-3"},ee={class:"flex items-center gap-2"},te={class:"flex-1 font-mono text-sm text-white break-all bg-surface px-3 py-2 rounded border border-border"},se={key:1,class:"bg-background border border-border rounded-lg p-5 space-y-4"},oe={class:"grid grid-cols-1 md:grid-cols-2 gap-3"},ae={class:"flex gap-2 pt-1"},re={class:"bg-background border border-border rounded-lg overflow-x-auto"},ne={class:"sm:hidden divide-y divide-border"},le={class:"flex items-start justify-between gap-2"},de={class:"min-w-0 flex-1"},ie={class:"flex items-center gap-2 flex-wrap"},ce={class:"font-medium text-white truncate"},ue={key:0,class:"text-[11px] font-mono text-foreground-muted bg-surface px-1.5 py-0.5 rounded"},pe={class:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-foreground-muted"},xe={key:0},me={key:1,class:"text-amber-400/80"},fe={key:2},ye={key:3,class:"text-red-400"},ve={key:4},ge={key:0,class:"px-6 py-8 text-center text-sm text-foreground-muted"},be={class:"hidden sm:table w-full text-sm text-left"},_e={class:"divide-y divide-border"},he={class:"px-6 py-4 text-white font-medium"},we={class:"px-6 py-4 text-foreground-muted font-mono text-xs hidden sm:table-cell"},ke={class:"px-6 py-4 text-foreground-muted hidden xl:table-cell"},Ce={class:"px-6 py-4 hidden md:table-cell"},De={key:0,class:"text-foreground-muted"},Ke={key:1,class:"text-amber-400/70 text-xs"},Ne={class:"px-6 py-4 hidden lg:table-cell"},Ie={key:0,class:"text-foreground-muted"},Ae={key:1,class:"text-red-400 text-xs"},Pe={key:2,class:"text-foreground-muted"},Te={class:"px-6 py-4 text-right"},Ee={key:0},Ge={__name:"ApiKeys",setup(Se){const v=L(),m=p([]),u=p(""),f=p(!1),g=p(!1),b=p(!1),d=p({name:"",expiresInDays:0}),_=async()=>{const r=await R();m.value=r.data.keys||[]},T=()=>{d.value={name:"",expiresInDays:0},g.value=!0},E=()=>{g.value=!1,d.value={name:"",expiresInDays:0}},S=async()=>{b.value=!0;try{const r={name:d.value.name.trim()};d.value.expiresInDays>0&&(r.expires_in_days=d.value.expiresInDays);const t=await G(r);u.value=t.data.key,f.value=!1,g.value=!1,await _()}catch(r){console.error(r),v.notify({title:"Failed to create key",message:r?.response?.data?.error?.message||"Unknown error",danger:!0})}finally{b.value=!1}},U=async()=>{await X(u.value)?(f.value=!0,setTimeout(()=>{f.value=!1},1500)):v.notify({title:"Copy failed",message:`Could not copy to clipboard. Select the key manually: + +`+u.value})},k=async r=>{if(await v.ask({title:"Delete API key?",message:`"${r.name||r.id}" will stop working immediately. This cannot be undone.`,confirmLabel:"Delete",danger:!0}))try{await H(r.id),await _()}catch(s){console.error(s),v.notify({title:"Failed to delete key",message:s?.response?.data?.error?.message||"Unknown error",danger:!0})}},V=r=>new Date(r).toLocaleString();return M(_),(r,t)=>(o(),a("div",J,[e("div",O,[t[4]||(t[4]=e("div",null,[e("h1",{class:"text-xl font-semibold text-white tracking-tight"}," API Keys "),e("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"}," Long-lived bearer tokens that authorise REST and MCP calls from CI, scripts, and external services. Plaintext is shown once at creation; the server keeps only a SHA-256 hash. ")],-1)),c(w,{onClick:T},{default:h(()=>[c(n(z),{class:"w-4 h-4"}),t[3]||(t[3]=i(" New Key ",-1))]),_:1})]),u.value?(o(),a("div",W,[e("div",Z,[t[5]||(t[5]=e("div",null,[e("h2",{class:"text-xs font-bold text-amber-300 uppercase tracking-wider"}," Copy this key now "),e("div",{class:"text-xs text-foreground-muted mt-0.5"}," It will not be shown again. Anyone with this key can invoke your functions. ")],-1)),e("button",{class:"text-foreground-muted hover:text-white",title:"Dismiss",onClick:t[0]||(t[0]=s=>u.value="")},[c(n(j),{class:"w-4 h-4"})])]),e("div",ee,[e("code",te,l(u.value),1),e("button",{class:"px-3 py-2 rounded-md border border-border bg-surface-hover hover:bg-surface text-foreground-muted hover:text-white transition-colors flex items-center gap-1.5 text-xs",onClick:U},[f.value?(o(),C(n(Y),{key:0,class:"w-3.5 h-3.5 text-success"})):(o(),C(n(q),{key:1,class:"w-3.5 h-3.5"})),i(" "+l(f.value?"Copied":"Copy"),1)])])])):y("",!0),g.value?(o(),a("div",se,[t[11]||(t[11]=e("div",{class:"text-sm font-semibold text-white"}," New API Key ",-1)),e("div",oe,[e("div",null,[t[6]||(t[6]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"Name",-1)),D(e("input",{"onUpdate:modelValue":t[1]||(t[1]=s=>d.value.name=s),placeholder:"e.g. ci-deployer",class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:border-white"},null,512),[[B,d.value.name]])]),e("div",null,[t[8]||(t[8]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"Expires in",-1)),D(e("select",{"onUpdate:modelValue":t[2]||(t[2]=s=>d.value.expiresInDays=s),class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:border-white"},[...t[7]||(t[7]=[e("option",{value:0}," Never ",-1),e("option",{value:1}," 1 day ",-1),e("option",{value:7}," 7 days ",-1),e("option",{value:30}," 30 days ",-1),e("option",{value:90}," 90 days ",-1),e("option",{value:365}," 1 year ",-1)])],512),[[F,d.value.expiresInDays]])])]),e("div",ae,[c(w,{disabled:!d.value.name.trim()||b.value,loading:b.value,onClick:S},{default:h(()=>[...t[9]||(t[9]=[i(" Generate Key ",-1)])]),_:1},8,["disabled","loading"]),c(w,{variant:"secondary",onClick:E},{default:h(()=>[...t[10]||(t[10]=[i(" Cancel ",-1)])]),_:1})])])):y("",!0),e("div",re,[e("ul",ne,[(o(!0),a(K,null,N(m.value,s=>(o(),a("li",{key:s.id,class:"px-4 py-3"},[e("div",le,[e("div",de,[e("div",ie,[e("span",ce,l(s.name||"Unnamed"),1),s.prefix?(o(),a("code",ue,l(s.prefix)+"…",1)):y("",!0)]),e("div",pe,[s.last_used_at?(o(),a("span",xe,"used "+l(n(x)(s.last_used_at)),1)):(o(),a("span",me,"never used")),s.expires_at?n(A)(s.expires_at)?(o(),a("span",ye,"expired "+l(n(x)(s.expires_at)),1)):(o(),a("span",ve,"expires "+l(n(x)(s.expires_at)),1)):(o(),a("span",fe,"no expiry"))])]),c(I,{icon:n(P),variant:"danger",title:"Delete key",onClick:$=>k(s)},null,8,["icon","onClick"])])]))),128)),m.value.length===0?(o(),a("li",ge,[...t[12]||(t[12]=[i(" No API keys yet. Tap ",-1),e("span",{class:"text-white"},"New Key",-1),i(" to generate one. ",-1)])])):y("",!0)]),e("table",be,[t[14]||(t[14]=e("thead",{class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},[e("tr",null,[e("th",{scope:"col",class:"px-6 py-3 font-medium"}," Name "),e("th",{scope:"col",class:"px-6 py-3 font-medium hidden sm:table-cell"}," Prefix "),e("th",{scope:"col",class:"px-6 py-3 font-medium hidden xl:table-cell"}," Created "),e("th",{scope:"col",class:"px-6 py-3 font-medium hidden md:table-cell"}," Last Used "),e("th",{scope:"col",class:"px-6 py-3 font-medium hidden lg:table-cell"}," Expires "),e("th",{scope:"col",class:"px-6 py-3 font-medium text-right"}," Actions ")])],-1)),e("tbody",_e,[(o(!0),a(K,null,N(m.value,s=>(o(),a("tr",{key:s.id,class:"hover:bg-surface/50 transition-colors"},[e("td",he,l(s.name||"Unnamed"),1),e("td",we,l(s.prefix?s.prefix+"…":n(Q)),1),e("td",ke,l(V(s.created_at)),1),e("td",Ce,[s.last_used_at?(o(),a("span",De,l(n(x)(s.last_used_at)),1)):(o(),a("span",Ke,"Never used"))]),e("td",Ne,[s.expires_at?n(A)(s.expires_at)?(o(),a("span",Ae,"Expired "+l(n(x)(s.expires_at)),1)):(o(),a("span",Pe,l(n(x)(s.expires_at)),1)):(o(),a("span",Ie,"Never"))]),e("td",Te,[c(I,{icon:n(P),variant:"danger",title:"Delete key",onClick:$=>k(s)},null,8,["icon","onClick"])])]))),128)),m.value.length===0?(o(),a("tr",Ee,[...t[13]||(t[13]=[e("td",{colspan:"6",class:"px-6 py-8 text-center text-foreground-muted"},[i(" No API keys yet. Click "),e("span",{class:"text-white"},"New Key"),i(" to generate one. ")],-1)])])):y("",!0)])])])]))}};export{Ge as default}; diff --git a/backend/internal/server/ui_dist/assets/Channels-BChFfeot.js b/backend/internal/server/ui_dist/assets/Channels-BChFfeot.js new file mode 100644 index 0000000..dce2dc2 --- /dev/null +++ b/backend/internal/server/ui_dist/assets/Channels-BChFfeot.js @@ -0,0 +1 @@ +import{r as f,o as H,a0 as Q,j as o,a as n,b as e,k as u,d,f as r,ac as K,$ as U,e as L,v as j,F as N,t as i,p as E,s as Y,w as z,g as v,h as w,_ as C,q as F,G as Z,n as S,V as ee,aS as te,a2 as se,B as V,aT as oe,aU as ne,aV as ae}from"./index-CmY58qNN.js";import{_ as I}from"./IconButton-i2ilFBbf.js";import{c as ie}from"./clipboard-CmSw2rR-.js";import{f as $,i as q}from"./time-Cfu9zNbw.js";import{C as le}from"./check-z8qyH5P-.js";import{C as re}from"./copy-BzujsGZw.js";import{C as de}from"./circle-alert-zkHrlU5I.js";import{R as G}from"./rotate-ccw-DQrtkmfg.js";import{T as O}from"./trash-2-BJzRpJ7Y.js";const ue={class:"w-full max-w-2xl bg-background border border-border rounded-lg shadow-lg flex flex-col max-h-[80vh]"},ce={class:"px-5 py-4 border-b border-border flex items-start justify-between gap-3"},pe={class:"px-5 py-3 border-b border-border flex items-center gap-2"},me={class:"flex-1 overflow-y-auto"},xe={key:0,class:"px-5 py-10 text-center text-xs text-foreground-muted italic"},fe={key:1,class:"px-5 py-10 text-center"},ve={class:"text-xs text-foreground-muted"},ge={key:2,class:"divide-y divide-border"},he=["onClick"],ye=["checked","onClick"],be={class:"flex-1 min-w-0"},_e={class:"text-sm font-medium text-white truncate"},ke={key:0,class:"text-xs text-foreground-muted mt-0.5 line-clamp-1"},we={class:"text-[11px] text-foreground-muted font-mono shrink-0"},Ce={class:"px-5 py-3 border-t border-border flex items-center justify-between gap-3"},$e={class:"text-xs text-foreground-muted tabular-nums"},Ne={class:"flex gap-2"},De={__name:"FunctionPickerModal",props:{selected:{type:Array,default:()=>[]}},emits:["close","apply"],setup(M,{emit:D}){const b=M,h=D,g=f([]),_=f(!0),y=f(""),x=f(new Set(b.selected)),k=c=>{const a=new Set(x.value);a.has(c)?a.delete(c):a.add(c),x.value=a},l=F(()=>{const c=y.value.trim().toLowerCase();return c?g.value.filter(a=>a.name.toLowerCase().includes(c)||(a.description||"").toLowerCase().includes(c)||(a.runtime||"").toLowerCase().includes(c)):g.value}),A=()=>{h("apply",Array.from(x.value))};return H(async()=>{try{const c=await Q({limit:200});g.value=c.data.functions||[]}finally{_.value=!1}}),(c,a)=>(o(),n("div",{class:"fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm",onClick:a[3]||(a[3]=z(p=>c.$emit("close"),["self"]))},[e("div",ue,[e("div",ce,[a[4]||(a[4]=e("div",null,[e("div",{class:"text-sm font-semibold text-white"}," Pick functions "),e("div",{class:"text-xs text-foreground-muted mt-0.5 max-w-prose leading-relaxed"},[u(" Each selected function becomes one MCP tool in this channel. Names with dashes are converted to snake_case (e.g. "),e("code",{class:"text-foreground"},"stripe-charge"),u(" → "),e("code",{class:"text-foreground"},"stripe_charge"),u("). ")])],-1)),e("button",{class:"text-foreground-muted hover:text-white transition-colors",title:"Dismiss",onClick:a[0]||(a[0]=p=>c.$emit("close"))},[d(r(K),{class:"w-4 h-4"})])]),e("div",pe,[d(r(U),{class:"w-4 h-4 text-foreground-muted shrink-0"}),L(e("input",{"onUpdate:modelValue":a[1]||(a[1]=p=>y.value=p),type:"text",placeholder:"Filter by name, description, or runtime",class:"flex-1 bg-transparent text-sm text-foreground placeholder-foreground-muted focus:outline-none"},null,512),[[j,y.value]])]),e("div",me,[_.value?(o(),n("div",xe," Loading functions… ")):l.value.length===0?(o(),n("div",fe,[d(r(U),{class:"w-8 h-8 text-foreground-muted mx-auto mb-2 opacity-30"}),e("p",ve,[g.value.length===0?(o(),n(N,{key:0},[u(" No functions deployed yet. ")],64)):(o(),n(N,{key:1},[u(' No functions match "'+i(y.value)+'". ',1)],64))])])):(o(),n("ul",ge,[(o(!0),n(N,null,E(l.value,p=>(o(),n("li",{key:p.id,class:Y(["px-5 py-3 flex items-center gap-3 cursor-pointer transition-colors",x.value.has(p.id)?"bg-surface/30 hover:bg-surface/50":"hover:bg-surface/40"]),onClick:P=>k(p.id)},[e("input",{type:"checkbox",checked:x.value.has(p.id),class:"accent-primary cursor-pointer",onClick:z(P=>k(p.id),["stop"])},null,8,ye),e("div",be,[e("div",_e,i(p.name),1),p.description?(o(),n("div",ke,i(p.description),1)):v("",!0)]),e("code",we,i(p.runtime),1)],10,he))),128))]))]),e("div",Ce,[e("div",$e,i(x.value.size)+" of "+i(g.value.length)+" selected ",1),e("div",Ne,[d(C,{variant:"secondary",onClick:a[2]||(a[2]=p=>c.$emit("close"))},{default:w(()=>[...a[5]||(a[5]=[u(" Cancel ",-1)])]),_:1}),d(C,{disabled:x.value.size===0,onClick:A},{default:w(()=>[...a[6]||(a[6]=[u(" Apply ",-1)])]),_:1},8,["disabled"])])])])]))}},Ie={class:"space-y-6"},Le={class:"flex items-center justify-between gap-4"},Ae={key:0,class:"bg-background border border-warning-ring rounded-lg p-4 space-y-3"},Pe={class:"flex items-start justify-between gap-3"},Re={class:"flex items-center gap-2"},Se={class:"flex-1 font-mono text-sm text-white break-all bg-surface px-3 py-2 rounded border border-border"},Ve={class:"text-[11px] text-foreground-muted flex flex-wrap items-center gap-x-3 gap-y-1"},je={class:"text-foreground bg-surface px-1.5 py-0.5 rounded"},Ee={key:1,class:"bg-background border border-border rounded-lg p-5 space-y-4"},Fe={class:"grid grid-cols-1 md:grid-cols-2 gap-3"},Me={class:"flex items-center justify-between mb-1.5"},Te={key:0,class:"text-[11px] text-foreground-muted"},Be={key:0,class:"rounded-md border border-red-700/40 bg-red-950/30 p-3 text-xs text-red-200 flex items-start gap-2"},Ue={class:"flex gap-2 pt-1"},ze={class:"bg-background border border-border rounded-lg overflow-x-auto"},qe={class:"sm:hidden divide-y divide-border"},Ge={class:"flex items-start justify-between gap-2"},Oe={class:"min-w-0 flex-1"},He={class:"flex items-center gap-2 flex-wrap"},Ke={class:"font-medium text-white truncate"},We={class:"inline-flex items-center gap-1 text-[11px] text-foreground-muted"},Xe={key:0,class:"mt-1 text-xs text-foreground-muted line-clamp-2"},Je={class:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-foreground-muted"},Qe={class:"font-mono"},Ye={key:0},Ze={key:1,class:"text-amber-400/80"},et={key:2,class:"text-red-400"},tt={key:3},st={class:"flex items-center gap-1 shrink-0"},ot={key:0,class:"px-6 py-8 text-center text-sm text-foreground-muted"},nt={class:"hidden sm:table w-full text-sm text-left"},at={class:"divide-y divide-border"},it={class:"px-6 py-4"},lt={class:"font-medium text-white"},rt={key:0,class:"text-xs text-foreground-muted mt-0.5 line-clamp-1 max-w-md"},dt={class:"px-6 py-4"},ut={class:"inline-flex items-center gap-1.5 text-foreground-muted"},ct={class:"tabular-nums"},pt={class:"px-6 py-4 hidden sm:table-cell"},mt={class:"text-foreground-muted font-mono text-xs"},xt={class:"px-6 py-4 hidden md:table-cell"},ft={key:0,class:"text-foreground-muted"},vt={key:1,class:"text-amber-400/70 text-xs"},gt={class:"px-6 py-4 hidden lg:table-cell"},ht={key:0,class:"text-foreground-muted"},yt={key:1,class:"text-red-400 text-xs"},bt={key:2,class:"text-foreground-muted"},_t={class:"px-6 py-4 text-right"},kt={class:"inline-flex justify-end gap-1"},wt={key:0},St={__name:"Channels",setup(M){const D=Z(),b=f([]),h=f(""),g=f(!1),_=f(!1),y=f(!1),x=f(""),k=f(!1),l=f({name:"",description:"",expiresInDays:0,functionIds:[]}),A=F(()=>`${window.location.origin}/mcp`),c=F(()=>l.value.name.trim()&&l.value.functionIds.length>0),a=async()=>{const m=await te();b.value=m.data.channels||[]},p=()=>{l.value={name:"",description:"",expiresInDays:0,functionIds:[]},x.value="",_.value=!0},P=()=>{_.value=!1},W=m=>{l.value.functionIds=m,k.value=!1},X=async()=>{y.value=!0,x.value="";try{const m={name:l.value.name.trim(),description:l.value.description.trim(),function_ids:l.value.functionIds};l.value.expiresInDays>0&&(m.expires_in_days=l.value.expiresInDays);const t=await oe(m);h.value=t.data.token,_.value=!1,await a()}catch(m){x.value=m?.response?.data?.error?.message||"Failed to create channel."}finally{y.value=!1}},J=async()=>{h.value&&await ie(h.value)&&(g.value=!0,setTimeout(()=>{g.value=!1},1500))},T=async m=>{if(!await D.ask({title:`Rotate ${m.name}?`,message:"A new token will be issued. The previous token stops working immediately. Agents using it will need the new value.",confirmLabel:"Rotate",danger:!0}))return;const s=await ne(m.id);h.value=s.data.token,await a()},B=async m=>{await D.ask({title:`Delete ${m.name}?`,message:`${m.name} will lose MCP access immediately. Functions inside are not affected. Re-create the channel if you need it again.`,confirmLabel:"Delete",danger:!0})&&(await ae(m.id),await a())};return H(a),(m,t)=>(o(),n("div",Ie,[e("div",Le,[t[7]||(t[7]=e("div",null,[e("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Channels "),e("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"}," Bundle deployed functions and expose them as MCP tools to a third-party agent. Each channel has its own bearer token that grants invoke-only access to its functions and nothing else on Orva, but the bundled functions themselves remain as powerful as you've configured them, including any in-sandbox SDK calls they make. ")],-1)),d(C,{onClick:p},{default:w(()=>[d(r(se),{class:"w-4 h-4"}),t[6]||(t[6]=u(" New channel ",-1))]),_:1})]),h.value?(o(),n("div",Ae,[e("div",Pe,[t[8]||(t[8]=e("div",null,[e("h2",{class:"text-xs font-bold text-warning-fg uppercase tracking-wider"}," Copy this token now "),e("div",{class:"text-xs text-foreground-muted mt-0.5"}," It will not be shown again. Configure it in your agent's MCP client. ")],-1)),e("button",{class:"text-foreground-muted hover:text-white transition-colors",title:"Dismiss",onClick:t[0]||(t[0]=s=>h.value="")},[d(r(K),{class:"w-4 h-4"})])]),e("div",Re,[e("code",Se,i(h.value),1),e("button",{class:"px-3 py-2 rounded-md border border-border bg-surface-hover hover:bg-surface text-foreground-muted hover:text-white transition-colors flex items-center gap-1.5 text-xs",onClick:J},[g.value?(o(),S(r(le),{key:0,class:"w-3.5 h-3.5 text-success"})):(o(),S(r(re),{key:1,class:"w-3.5 h-3.5"})),u(" "+i(g.value?"Copied":"Copy"),1)])]),e("div",Ve,[e("span",null,[t[9]||(t[9]=u("URL ",-1)),e("code",je,i(A.value),1)]),t[10]||(t[10]=e("span",null,[u("Header "),e("code",{class:"text-foreground bg-surface px-1.5 py-0.5 rounded"},"Authorization: Bearer ")],-1))])])):v("",!0),_.value?(o(),n("div",Ee,[t[18]||(t[18]=e("div",{class:"text-sm font-semibold text-white"}," New channel ",-1)),e("div",Fe,[e("div",null,[t[11]||(t[11]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"Name",-1)),L(e("input",{"onUpdate:modelValue":t[1]||(t[1]=s=>l.value.name=s),placeholder:"e.g. support-bot",class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary transition-colors"},null,512),[[j,l.value.name]])]),e("div",null,[t[13]||(t[13]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"Expires in",-1)),L(e("select",{"onUpdate:modelValue":t[2]||(t[2]=s=>l.value.expiresInDays=s),class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary transition-colors"},[...t[12]||(t[12]=[e("option",{value:0}," Never ",-1),e("option",{value:7}," 7 days ",-1),e("option",{value:30}," 30 days ",-1),e("option",{value:90}," 90 days ",-1)])],512),[[ee,l.value.expiresInDays]])])]),e("div",null,[t[14]||(t[14]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"Description (optional)",-1)),L(e("input",{"onUpdate:modelValue":t[3]||(t[3]=s=>l.value.description=s),placeholder:"What this channel is for",class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary transition-colors"},null,512),[[j,l.value.description]])]),e("div",null,[e("div",Me,[t[15]||(t[15]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide"},"Functions",-1)),l.value.functionIds.length>0?(o(),n("span",Te,i(l.value.functionIds.length)+" selected",1)):v("",!0)]),d(C,{variant:"secondary",onClick:t[4]||(t[4]=s=>k.value=!0)},{default:w(()=>[d(r(V),{class:"w-4 h-4"}),u(" "+i(l.value.functionIds.length===0?"Pick functions":"Edit selection"),1)]),_:1})]),x.value?(o(),n("div",Be,[d(r(de),{class:"w-4 h-4 text-red-400 shrink-0 mt-0.5"}),e("span",null,i(x.value),1)])):v("",!0),e("div",Ue,[d(C,{disabled:!c.value||y.value,loading:y.value,onClick:X},{default:w(()=>[...t[16]||(t[16]=[u(" Generate token ",-1)])]),_:1},8,["disabled","loading"]),d(C,{variant:"secondary",onClick:P},{default:w(()=>[...t[17]||(t[17]=[u(" Cancel ",-1)])]),_:1})])])):v("",!0),e("div",ze,[e("ul",qe,[(o(!0),n(N,null,E(b.value,s=>(o(),n("li",{key:s.id,class:"px-4 py-3"},[e("div",Ge,[e("div",Oe,[e("div",He,[e("span",Ke,i(s.name),1),e("span",We,[d(r(V),{class:"w-3 h-3"}),u(" "+i(s.function_count),1)])]),s.description?(o(),n("div",Xe,i(s.description),1)):v("",!0),e("div",Je,[e("code",Qe,i(s.prefix)+"…",1),s.last_used_at?(o(),n("span",Ye,"used "+i(r($)(s.last_used_at)),1)):(o(),n("span",Ze,"never used")),s.expires_at&&r(q)(s.expires_at)?(o(),n("span",et,"expired")):s.expires_at?(o(),n("span",tt,"expires "+i(r($)(s.expires_at)),1)):v("",!0)])]),e("div",st,[d(I,{icon:r(G),title:"Rotate token",onClick:R=>T(s)},null,8,["icon","onClick"]),d(I,{icon:r(O),variant:"danger",title:"Delete channel",onClick:R=>B(s)},null,8,["icon","onClick"])])])]))),128)),b.value.length===0?(o(),n("li",ot," No channels yet. ")):v("",!0)]),e("table",nt,[t[20]||(t[20]=e("thead",{class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},[e("tr",null,[e("th",{class:"px-6 py-3 font-medium"}," Name "),e("th",{class:"px-6 py-3 font-medium"}," Functions "),e("th",{class:"px-6 py-3 font-medium hidden sm:table-cell"}," Prefix "),e("th",{class:"px-6 py-3 font-medium hidden md:table-cell"}," Last used "),e("th",{class:"px-6 py-3 font-medium hidden lg:table-cell"}," Expires "),e("th",{class:"px-6 py-3 font-medium text-right"}," Actions ")])],-1)),e("tbody",at,[(o(!0),n(N,null,E(b.value,s=>(o(),n("tr",{key:s.id,class:"hover:bg-surface/50 transition-colors"},[e("td",it,[e("div",lt,i(s.name),1),s.description?(o(),n("div",rt,i(s.description),1)):v("",!0)]),e("td",dt,[e("span",ut,[d(r(V),{class:"w-3.5 h-3.5"}),e("span",ct,i(s.function_count),1)])]),e("td",pt,[e("code",mt,i(s.prefix)+"…",1)]),e("td",xt,[s.last_used_at?(o(),n("span",ft,i(r($)(s.last_used_at)),1)):(o(),n("span",vt,"Never used"))]),e("td",gt,[s.expires_at?r(q)(s.expires_at)?(o(),n("span",yt,"Expired "+i(r($)(s.expires_at)),1)):(o(),n("span",bt,i(r($)(s.expires_at)),1)):(o(),n("span",ht,"Never"))]),e("td",_t,[e("div",kt,[d(I,{icon:r(G),title:"Rotate token",onClick:R=>T(s)},null,8,["icon","onClick"]),d(I,{icon:r(O),variant:"danger",title:"Delete channel",onClick:R=>B(s)},null,8,["icon","onClick"])])])]))),128)),b.value.length===0?(o(),n("tr",wt,[...t[19]||(t[19]=[e("td",{colspan:"6",class:"px-6 py-8 text-center text-foreground-muted"},[u(" No channels yet. Click "),e("span",{class:"text-white"},"New channel"),u(" to bundle functions for an agent. ")],-1)])])):v("",!0)])])]),k.value?(o(),S(De,{key:2,selected:l.value.functionIds,onClose:t[5]||(t[5]=s=>k.value=!1),onApply:W},null,8,["selected"])):v("",!0)]))}};export{St as default}; diff --git a/backend/internal/server/ui_dist/assets/Channels-DE_p7CG8.js b/backend/internal/server/ui_dist/assets/Channels-DE_p7CG8.js deleted file mode 100644 index a9d4810..0000000 --- a/backend/internal/server/ui_dist/assets/Channels-DE_p7CG8.js +++ /dev/null @@ -1 +0,0 @@ -import{r as f,o as G,H as J,j as o,a as n,b as e,k as u,d,f as r,a3 as K,S as U,e as R,v as E,F as N,t as l,p as F,s as Y,w as z,g as v,h as w,_ as C,q as M,C as Z,n as S,R as ee,aO as te,N as se,B as j,aP as oe,aQ as ne,aR as ae}from"./index-WXhXpu06.js";import{_ as I}from"./IconButton-CclydhPm.js";import{c as le}from"./clipboard-CmSw2rR-.js";import{f as $,i as O}from"./time-Cfu9zNbw.js";import{C as ie}from"./check-CuO1EVch.js";import{C as re}from"./copy-HWkx-vox.js";import{C as de}from"./circle-alert-CvukaQh2.js";import{R as q}from"./rotate-ccw-Cx2X0X0k.js";import{T as H}from"./trash-2-dFLn8UYZ.js";const ue={class:"w-full max-w-2xl bg-background border border-border rounded-lg shadow-2xl shadow-black/50 flex flex-col max-h-[80vh]"},ce={class:"px-5 py-4 border-b border-border flex items-start justify-between gap-3"},pe={class:"px-5 py-3 border-b border-border flex items-center gap-2"},me={class:"flex-1 overflow-y-auto"},xe={key:0,class:"px-5 py-10 text-center text-xs text-foreground-muted italic"},fe={key:1,class:"px-5 py-10 text-center"},ve={class:"text-xs text-foreground-muted"},ge={key:2,class:"divide-y divide-border"},he=["onClick"],be=["checked","onClick"],ye={class:"flex-1 min-w-0"},_e={class:"text-sm font-medium text-white truncate"},ke={key:0,class:"text-xs text-foreground-muted mt-0.5 line-clamp-1"},we={class:"text-[11px] text-foreground-muted font-mono shrink-0"},Ce={class:"px-5 py-3 border-t border-border flex items-center justify-between gap-3"},$e={class:"text-xs text-foreground-muted tabular-nums"},Ne={class:"flex gap-2"},De={__name:"FunctionPickerModal",props:{selected:{type:Array,default:()=>[]}},emits:["close","apply"],setup(B,{emit:D}){const y=B,h=D,g=f([]),_=f(!0),b=f(""),x=f(new Set(y.selected)),k=c=>{const a=new Set(x.value);a.has(c)?a.delete(c):a.add(c),x.value=a},i=M(()=>{const c=b.value.trim().toLowerCase();return c?g.value.filter(a=>a.name.toLowerCase().includes(c)||(a.description||"").toLowerCase().includes(c)||(a.runtime||"").toLowerCase().includes(c)):g.value}),L=()=>{h("apply",Array.from(x.value))};return G(async()=>{try{const c=await J({limit:200});g.value=c.data.functions||[]}finally{_.value=!1}}),(c,a)=>(o(),n("div",{class:"fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm",onClick:a[3]||(a[3]=z(p=>c.$emit("close"),["self"]))},[e("div",ue,[e("div",ce,[a[4]||(a[4]=e("div",null,[e("div",{class:"text-sm font-semibold text-white"}," Pick functions "),e("div",{class:"text-xs text-foreground-muted mt-0.5 max-w-prose leading-relaxed"},[u(" Each selected function becomes one MCP tool in this channel. Names with dashes are converted to snake_case (e.g. "),e("code",{class:"text-foreground"},"stripe-charge"),u(" → "),e("code",{class:"text-foreground"},"stripe_charge"),u("). ")])],-1)),e("button",{class:"text-foreground-muted hover:text-white transition-colors",title:"Dismiss",onClick:a[0]||(a[0]=p=>c.$emit("close"))},[d(r(K),{class:"w-4 h-4"})])]),e("div",pe,[d(r(U),{class:"w-4 h-4 text-foreground-muted shrink-0"}),R(e("input",{"onUpdate:modelValue":a[1]||(a[1]=p=>b.value=p),type:"text",placeholder:"Filter by name, description, or runtime",class:"flex-1 bg-transparent text-sm text-foreground placeholder-foreground-muted focus:outline-none"},null,512),[[E,b.value]])]),e("div",me,[_.value?(o(),n("div",xe," Loading functions… ")):i.value.length===0?(o(),n("div",fe,[d(r(U),{class:"w-8 h-8 text-foreground-muted mx-auto mb-2 opacity-30"}),e("p",ve,[g.value.length===0?(o(),n(N,{key:0},[u(" No functions deployed yet. ")],64)):(o(),n(N,{key:1},[u(' No functions match "'+l(b.value)+'". ',1)],64))])])):(o(),n("ul",ge,[(o(!0),n(N,null,F(i.value,p=>(o(),n("li",{key:p.id,class:Y(["px-5 py-3 flex items-center gap-3 cursor-pointer transition-colors",x.value.has(p.id)?"bg-surface/30 hover:bg-surface/50":"hover:bg-surface/40"]),onClick:P=>k(p.id)},[e("input",{type:"checkbox",checked:x.value.has(p.id),class:"accent-primary cursor-pointer",onClick:z(P=>k(p.id),["stop"])},null,8,be),e("div",ye,[e("div",_e,l(p.name),1),p.description?(o(),n("div",ke,l(p.description),1)):v("",!0)]),e("code",we,l(p.runtime),1)],10,he))),128))]))]),e("div",Ce,[e("div",$e,l(x.value.size)+" of "+l(g.value.length)+" selected ",1),e("div",Ne,[d(C,{variant:"secondary",onClick:a[2]||(a[2]=p=>c.$emit("close"))},{default:w(()=>[...a[5]||(a[5]=[u(" Cancel ",-1)])]),_:1}),d(C,{disabled:x.value.size===0,onClick:L},{default:w(()=>[...a[6]||(a[6]=[u(" Apply ",-1)])]),_:1},8,["disabled"])])])])]))}},Ie={class:"space-y-6"},Re={class:"flex items-center justify-between gap-4"},Le={key:0,class:"bg-background border border-amber-700/40 rounded-lg p-4 space-y-3"},Pe={class:"flex items-start justify-between gap-3"},Ae={class:"flex items-center gap-2"},Se={class:"flex-1 font-mono text-sm text-white break-all bg-surface px-3 py-2 rounded border border-border"},je={class:"text-[11px] text-foreground-muted flex flex-wrap items-center gap-x-3 gap-y-1"},Ee={class:"text-foreground bg-surface px-1.5 py-0.5 rounded"},Fe={key:1,class:"bg-background border border-border rounded-lg p-5 space-y-4"},Me={class:"grid grid-cols-1 md:grid-cols-2 gap-3"},Be={class:"flex items-center justify-between mb-1.5"},Te={key:0,class:"text-[11px] text-foreground-muted"},Ve={key:0,class:"rounded-md border border-red-700/40 bg-red-950/30 p-3 text-xs text-red-200 flex items-start gap-2"},Ue={class:"flex gap-2 pt-1"},ze={class:"bg-background border border-border rounded-lg overflow-x-auto"},Oe={class:"sm:hidden divide-y divide-border"},qe={class:"flex items-start justify-between gap-2"},He={class:"min-w-0 flex-1"},Ge={class:"flex items-center gap-2 flex-wrap"},Ke={class:"font-medium text-white truncate"},Qe={class:"inline-flex items-center gap-1 text-[11px] text-foreground-muted"},We={key:0,class:"mt-1 text-xs text-foreground-muted line-clamp-2"},Xe={class:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-foreground-muted"},Je={class:"font-mono"},Ye={key:0},Ze={key:1,class:"text-amber-400/80"},et={key:2,class:"text-red-400"},tt={key:3},st={class:"flex items-center gap-1 shrink-0"},ot={key:0,class:"px-6 py-8 text-center text-sm text-foreground-muted"},nt={class:"hidden sm:table w-full text-sm text-left"},at={class:"divide-y divide-border"},lt={class:"px-6 py-4"},it={class:"font-medium text-white"},rt={key:0,class:"text-xs text-foreground-muted mt-0.5 line-clamp-1 max-w-md"},dt={class:"px-6 py-4"},ut={class:"inline-flex items-center gap-1.5 text-foreground-muted"},ct={class:"tabular-nums"},pt={class:"px-6 py-4 hidden sm:table-cell"},mt={class:"text-foreground-muted font-mono text-xs"},xt={class:"px-6 py-4 hidden md:table-cell"},ft={key:0,class:"text-foreground-muted"},vt={key:1,class:"text-amber-400/70 text-xs"},gt={class:"px-6 py-4 hidden lg:table-cell"},ht={key:0,class:"text-foreground-muted"},bt={key:1,class:"text-red-400 text-xs"},yt={key:2,class:"text-foreground-muted"},_t={class:"px-6 py-4 text-right"},kt={class:"inline-flex justify-end gap-1"},wt={key:0},St={__name:"Channels",setup(B){const D=Z(),y=f([]),h=f(""),g=f(!1),_=f(!1),b=f(!1),x=f(""),k=f(!1),i=f({name:"",description:"",expiresInDays:0,functionIds:[]}),L=M(()=>`${window.location.origin}/mcp`),c=M(()=>i.value.name.trim()&&i.value.functionIds.length>0),a=async()=>{const m=await te();y.value=m.data.channels||[]},p=()=>{i.value={name:"",description:"",expiresInDays:0,functionIds:[]},x.value="",_.value=!0},P=()=>{_.value=!1},Q=m=>{i.value.functionIds=m,k.value=!1},W=async()=>{b.value=!0,x.value="";try{const m={name:i.value.name.trim(),description:i.value.description.trim(),function_ids:i.value.functionIds};i.value.expiresInDays>0&&(m.expires_in_days=i.value.expiresInDays);const t=await oe(m);h.value=t.data.token,_.value=!1,await a()}catch(m){x.value=m?.response?.data?.error?.message||"Failed to create channel."}finally{b.value=!1}},X=async()=>{h.value&&await le(h.value)&&(g.value=!0,setTimeout(()=>{g.value=!1},1500))},T=async m=>{if(!await D.ask({title:`Rotate ${m.name}?`,message:"A new token will be issued. The previous token stops working immediately. Agents using it will need the new value.",confirmLabel:"Rotate",danger:!0}))return;const s=await ne(m.id);h.value=s.data.token,await a()},V=async m=>{await D.ask({title:`Delete ${m.name}?`,message:`${m.name} will lose MCP access immediately. Functions inside are not affected. Re-create the channel if you need it again.`,confirmLabel:"Delete",danger:!0})&&(await ae(m.id),await a())};return G(a),(m,t)=>(o(),n("div",Ie,[e("div",Re,[t[7]||(t[7]=e("div",null,[e("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Channels "),e("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"}," Bundle deployed functions and expose them as MCP tools to a third-party agent. Each channel has its own bearer token that grants invoke-only access to its functions and nothing else on Orva, but the bundled functions themselves remain as powerful as you've configured them, including any in-sandbox SDK calls they make. ")],-1)),d(C,{onClick:p},{default:w(()=>[d(r(se),{class:"w-4 h-4"}),t[6]||(t[6]=u(" New channel ",-1))]),_:1})]),h.value?(o(),n("div",Le,[e("div",Pe,[t[8]||(t[8]=e("div",null,[e("h2",{class:"text-xs font-bold text-amber-300 uppercase tracking-wider"}," Copy this token now "),e("div",{class:"text-xs text-foreground-muted mt-0.5"}," It will not be shown again. Configure it in your agent's MCP client. ")],-1)),e("button",{class:"text-foreground-muted hover:text-white transition-colors",title:"Dismiss",onClick:t[0]||(t[0]=s=>h.value="")},[d(r(K),{class:"w-4 h-4"})])]),e("div",Ae,[e("code",Se,l(h.value),1),e("button",{class:"px-3 py-2 rounded-md border border-border bg-surface-hover hover:bg-surface text-foreground-muted hover:text-white transition-colors flex items-center gap-1.5 text-xs",onClick:X},[g.value?(o(),S(r(ie),{key:0,class:"w-3.5 h-3.5 text-success"})):(o(),S(r(re),{key:1,class:"w-3.5 h-3.5"})),u(" "+l(g.value?"Copied":"Copy"),1)])]),e("div",je,[e("span",null,[t[9]||(t[9]=u("URL ",-1)),e("code",Ee,l(L.value),1)]),t[10]||(t[10]=e("span",null,[u("Header "),e("code",{class:"text-foreground bg-surface px-1.5 py-0.5 rounded"},"Authorization: Bearer ")],-1))])])):v("",!0),_.value?(o(),n("div",Fe,[t[18]||(t[18]=e("div",{class:"text-sm font-semibold text-white"}," New channel ",-1)),e("div",Me,[e("div",null,[t[11]||(t[11]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"Name",-1)),R(e("input",{"onUpdate:modelValue":t[1]||(t[1]=s=>i.value.name=s),placeholder:"e.g. support-bot",class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary transition-colors"},null,512),[[E,i.value.name]])]),e("div",null,[t[13]||(t[13]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"Expires in",-1)),R(e("select",{"onUpdate:modelValue":t[2]||(t[2]=s=>i.value.expiresInDays=s),class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary transition-colors"},[...t[12]||(t[12]=[e("option",{value:0}," Never ",-1),e("option",{value:7}," 7 days ",-1),e("option",{value:30}," 30 days ",-1),e("option",{value:90}," 90 days ",-1)])],512),[[ee,i.value.expiresInDays]])])]),e("div",null,[t[14]||(t[14]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"Description (optional)",-1)),R(e("input",{"onUpdate:modelValue":t[3]||(t[3]=s=>i.value.description=s),placeholder:"What this channel is for",class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary transition-colors"},null,512),[[E,i.value.description]])]),e("div",null,[e("div",Be,[t[15]||(t[15]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide"},"Functions",-1)),i.value.functionIds.length>0?(o(),n("span",Te,l(i.value.functionIds.length)+" selected",1)):v("",!0)]),d(C,{variant:"secondary",onClick:t[4]||(t[4]=s=>k.value=!0)},{default:w(()=>[d(r(j),{class:"w-4 h-4"}),u(" "+l(i.value.functionIds.length===0?"Pick functions":"Edit selection"),1)]),_:1})]),x.value?(o(),n("div",Ve,[d(r(de),{class:"w-4 h-4 text-red-400 shrink-0 mt-0.5"}),e("span",null,l(x.value),1)])):v("",!0),e("div",Ue,[d(C,{disabled:!c.value||b.value,loading:b.value,onClick:W},{default:w(()=>[...t[16]||(t[16]=[u(" Generate token ",-1)])]),_:1},8,["disabled","loading"]),d(C,{variant:"secondary",onClick:P},{default:w(()=>[...t[17]||(t[17]=[u(" Cancel ",-1)])]),_:1})])])):v("",!0),e("div",ze,[e("ul",Oe,[(o(!0),n(N,null,F(y.value,s=>(o(),n("li",{key:s.id,class:"px-4 py-3"},[e("div",qe,[e("div",He,[e("div",Ge,[e("span",Ke,l(s.name),1),e("span",Qe,[d(r(j),{class:"w-3 h-3"}),u(" "+l(s.function_count),1)])]),s.description?(o(),n("div",We,l(s.description),1)):v("",!0),e("div",Xe,[e("code",Je,l(s.prefix)+"…",1),s.last_used_at?(o(),n("span",Ye,"used "+l(r($)(s.last_used_at)),1)):(o(),n("span",Ze,"never used")),s.expires_at&&r(O)(s.expires_at)?(o(),n("span",et,"expired")):s.expires_at?(o(),n("span",tt,"expires "+l(r($)(s.expires_at)),1)):v("",!0)])]),e("div",st,[d(I,{icon:r(q),title:"Rotate token",onClick:A=>T(s)},null,8,["icon","onClick"]),d(I,{icon:r(H),variant:"danger",title:"Delete channel",onClick:A=>V(s)},null,8,["icon","onClick"])])])]))),128)),y.value.length===0?(o(),n("li",ot," No channels yet. ")):v("",!0)]),e("table",nt,[t[20]||(t[20]=e("thead",{class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},[e("tr",null,[e("th",{class:"px-6 py-3 font-medium"}," Name "),e("th",{class:"px-6 py-3 font-medium"}," Functions "),e("th",{class:"px-6 py-3 font-medium hidden sm:table-cell"}," Prefix "),e("th",{class:"px-6 py-3 font-medium hidden md:table-cell"}," Last used "),e("th",{class:"px-6 py-3 font-medium hidden lg:table-cell"}," Expires "),e("th",{class:"px-6 py-3 font-medium text-right"}," Actions ")])],-1)),e("tbody",at,[(o(!0),n(N,null,F(y.value,s=>(o(),n("tr",{key:s.id,class:"hover:bg-surface/50 transition-colors"},[e("td",lt,[e("div",it,l(s.name),1),s.description?(o(),n("div",rt,l(s.description),1)):v("",!0)]),e("td",dt,[e("span",ut,[d(r(j),{class:"w-3.5 h-3.5"}),e("span",ct,l(s.function_count),1)])]),e("td",pt,[e("code",mt,l(s.prefix)+"…",1)]),e("td",xt,[s.last_used_at?(o(),n("span",ft,l(r($)(s.last_used_at)),1)):(o(),n("span",vt,"Never used"))]),e("td",gt,[s.expires_at?r(O)(s.expires_at)?(o(),n("span",bt,"Expired "+l(r($)(s.expires_at)),1)):(o(),n("span",yt,l(r($)(s.expires_at)),1)):(o(),n("span",ht,"Never"))]),e("td",_t,[e("div",kt,[d(I,{icon:r(q),title:"Rotate token",onClick:A=>T(s)},null,8,["icon","onClick"]),d(I,{icon:r(H),variant:"danger",title:"Delete channel",onClick:A=>V(s)},null,8,["icon","onClick"])])])]))),128)),y.value.length===0?(o(),n("tr",wt,[...t[19]||(t[19]=[e("td",{colspan:"6",class:"px-6 py-8 text-center text-foreground-muted"},[u(" No channels yet. Click "),e("span",{class:"text-white"},"New channel"),u(" to bundle functions for an agent. ")],-1)])])):v("",!0)])])]),k.value?(o(),S(De,{key:2,selected:i.value.functionIds,onClose:t[5]||(t[5]=s=>k.value=!1),onApply:Q},null,8,["selected"])):v("",!0)]))}};export{St as default}; diff --git a/backend/internal/server/ui_dist/assets/CodeEditor-Do1Z2Nkh.js b/backend/internal/server/ui_dist/assets/CodeEditor-se_FXC0j.js similarity index 90% rename from backend/internal/server/ui_dist/assets/CodeEditor-Do1Z2Nkh.js rename to backend/internal/server/ui_dist/assets/CodeEditor-se_FXC0j.js index 5b2bf1e..71d6703 100644 --- a/backend/internal/server/ui_dist/assets/CodeEditor-Do1Z2Nkh.js +++ b/backend/internal/server/ui_dist/assets/CodeEditor-se_FXC0j.js @@ -1 +1 @@ -import{a as d,p as m,q as g,C as h,E as n,r as y,u as l}from"./index-BOWx3BJu.js";import{K as _,o as x,y as S,L as c,j as v,a as E,r as w}from"./index-WXhXpu06.js";const C={__name:"CodeEditor",props:{modelValue:{type:String,default:""},language:{type:String,default:"javascript"},readOnly:{type:Boolean,default:!1}},emits:["update:modelValue"],setup(p,{emit:f}){const a=p,u=f,s=w(null);let t=null;const r=new h,i=e=>e?.startsWith("python")?y():e?.startsWith("node")||e==="javascript"?l():l();return x(()=>{const e=d.create({doc:a.modelValue,extensions:[m,r.of(i(a.language)),g,n.updateListener.of(o=>{o.docChanged&&u("update:modelValue",o.state.doc.toString())}),n.theme({"&":{fontSize:"16px",height:"100%"},"@media (min-width: 640px)":{"&":{fontSize:"14px"}},".cm-scroller":{fontFamily:"JetBrains Mono, monospace",lineHeight:"1.6"},".cm-content":{padding:"16px 0"},".cm-line":{padding:"0 16px"}}),d.readOnly.of(a.readOnly)]});t=new n({state:e,parent:s.value})}),S(()=>{t&&t.destroy()}),c(()=>a.modelValue,e=>{t&&e!==t.state.doc.toString()&&t.dispatch({changes:{from:0,to:t.state.doc.length,insert:e}})}),c(()=>a.language,e=>{t&&t.dispatch({effects:r.reconfigure(i(e))})}),(e,o)=>(v(),E("div",{ref_key:"editorRef",ref:s,class:"h-full w-full"},null,512))}},k=_(C,[["__scopeId","data-v-7991b3c8"]]);export{k as default}; +import{a as d,p as m,q as g,C as h,E as n,r as y,u as l}from"./index-BOWx3BJu.js";import{H as _,o as x,y as S,I as c,j as v,a as E,r as w}from"./index-CmY58qNN.js";const C={__name:"CodeEditor",props:{modelValue:{type:String,default:""},language:{type:String,default:"javascript"},readOnly:{type:Boolean,default:!1}},emits:["update:modelValue"],setup(p,{emit:f}){const a=p,u=f,s=w(null);let t=null;const r=new h,i=e=>e?.startsWith("python")?y():e?.startsWith("node")||e==="javascript"?l():l();return x(()=>{const e=d.create({doc:a.modelValue,extensions:[m,r.of(i(a.language)),g,n.updateListener.of(o=>{o.docChanged&&u("update:modelValue",o.state.doc.toString())}),n.theme({"&":{fontSize:"16px",height:"100%"},"@media (min-width: 640px)":{"&":{fontSize:"14px"}},".cm-scroller":{fontFamily:"JetBrains Mono, monospace",lineHeight:"1.6"},".cm-content":{padding:"16px 0"},".cm-line":{padding:"0 16px"}}),d.readOnly.of(a.readOnly)]});t=new n({state:e,parent:s.value})}),S(()=>{t&&t.destroy()}),c(()=>a.modelValue,e=>{t&&e!==t.state.doc.toString()&&t.dispatch({changes:{from:0,to:t.state.doc.length,insert:e}})}),c(()=>a.language,e=>{t&&t.dispatch({effects:r.reconfigure(i(e))})}),(e,o)=>(v(),E("div",{ref_key:"editorRef",ref:s,class:"h-full w-full"},null,512))}},k=_(C,[["__scopeId","data-v-7991b3c8"]]);export{k as default}; diff --git a/backend/internal/server/ui_dist/assets/CronJobs-BfUMYQN-.js b/backend/internal/server/ui_dist/assets/CronJobs-BfUMYQN-.js deleted file mode 100644 index e3f6a2e..0000000 --- a/backend/internal/server/ui_dist/assets/CronJobs-BfUMYQN-.js +++ /dev/null @@ -1 +0,0 @@ -import{c as O,ao as K,C as X,o as Z,a,b as t,d as c,h as F,_ as U,F as S,p as $,f as i,g as p,t as s,a3 as Q,e as m,R as M,l as N,v as A,k as b,a4 as j,ap as ee,H as te,r as y,j as l,s as L,aq as W,ar as oe,as as ne}from"./index-WXhXpu06.js";import{E as I}from"./format-CsU4_SPu.js";import{_ as V}from"./IconButton-CclydhPm.js";import{P as re}from"./play-l-CnPJiF.js";import{S as se}from"./square-pen-BkfRkbC3.js";import{T as de}from"./trash-2-dFLn8UYZ.js";const ae=O("circle-plus",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M8 12h8",key:"1wcyev"}],["path",{d:"M12 8v8",key:"napkw2"}]]);const le=O("clock",[["path",{d:"M12 6v6l4 2",key:"mmk7yg"}],["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]]);const ue=O("pause",[["rect",{x:"14",y:"3",width:"5",height:"18",rx:"1",key:"kaeet6"}],["rect",{x:"5",y:"3",width:"5",height:"18",rx:"1",key:"1wsw3u"}]]),ie={class:"space-y-6"},ce={class:"flex items-center justify-between"},me={class:"bg-background border border-border rounded-lg overflow-x-auto"},fe={class:"w-full text-sm text-left"},pe={class:"divide-y divide-border"},xe={class:"px-6 py-4 font-medium text-foreground"},be={class:"px-6 py-4"},ye={class:"flex flex-col gap-1"},ve={class:"text-foreground font-mono text-xs"},ge={class:"text-foreground-muted text-[10px]"},he={class:"text-foreground-muted/70"},ke={class:"px-6 py-4 hidden sm:table-cell"},we={class:"px-6 py-4 text-foreground-muted text-xs hidden md:table-cell"},_e={class:"px-6 py-4 text-foreground-muted text-xs hidden lg:table-cell"},Ce={class:"px-6 py-4 text-right"},Se={class:"inline-flex items-center gap-1"},$e={key:0},Me={colspan:"6",class:"px-6 py-12 text-center"},Ae={key:0,class:"fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center z-50 p-4"},Te={class:"bg-surface border border-border rounded-lg w-full max-w-2xl shadow-2xl shadow-black/50 max-h-[90vh] overflow-y-auto"},Ee={class:"border-b border-border px-6 py-4 flex items-center justify-between sticky top-0 bg-surface"},ze={class:"text-lg font-semibold text-foreground"},Fe={class:"p-6 space-y-5"},Ue=["disabled"],Ve=["value"],Oe={class:"flex gap-2 bg-background rounded-lg p-1 border border-border"},Pe=["onClick"],qe={key:0,class:"space-y-4"},De={class:"grid grid-cols-3 gap-3"},Ne={key:0},Le={key:1},We={key:2},Ie={key:3},Je={class:"bg-background border border-border rounded-lg p-4"},Be={class:"font-mono text-sm text-foreground"},Re={class:"text-xs text-foreground-muted mt-1"},He={key:1,class:"space-y-3"},Ye={class:"bg-background border border-border rounded-lg p-4"},Ge={class:"text-xs text-foreground"},Ke=["value"],Xe={class:"text-xs text-foreground-muted mt-1.5"},Ze={class:"bg-surface px-1 rounded"},Qe={class:"flex items-center gap-3"},je={class:"border-t border-border px-6 py-4 flex items-center justify-end gap-3 bg-surface sticky bottom-0"},lt={__name:"CronJobs",setup(et){const k=K(),J=(()=>{const n=["UTC","America/Los_Angeles","America/New_York","America/Chicago","America/Denver","America/Sao_Paulo","Europe/London","Europe/Berlin","Europe/Paris","Europe/Moscow","Africa/Lagos","Africa/Cairo","Africa/Johannesburg","Asia/Dubai","Asia/Kolkata","Asia/Singapore","Asia/Shanghai","Asia/Tokyo","Australia/Sydney","Pacific/Auckland"];return[...new Set([k,...n])]})(),P=X(),T=y([]),q=y([]),w=y(!1),f=y(null),g=y("simple"),r=y({function_name:"",cron:"0 0 * * *",timezone:k,enabled:!0}),d=y({frequency:"day",minute:0,hour:0,dayOfWeek:1,dayOfMonth:1}),_=async()=>{try{const n=await ee();T.value=n.data.schedules||[]}catch(n){console.error("Failed to load cron jobs",n)}},B=async()=>{try{const n=await te();q.value=n.data.functions||[]}catch(n){console.error("Failed to load functions",n)}},v=()=>{const{frequency:n,minute:e,hour:o,dayOfWeek:u,dayOfMonth:x}=d.value;switch(n){case"minute":r.value.cron="* * * * *";break;case"hour":r.value.cron=`${e} * * * *`;break;case"day":r.value.cron=`${e} ${o} * * *`;break;case"week":r.value.cron=`${e} ${o} * * ${u}`;break;case"month":r.value.cron=`${e} ${o} ${x} * *`;break}},E=n=>{if(!n)return"Invalid expression";const e=n.trim().split(/\s+/);if(e.length!==5)return"Invalid format (use 5 fields)";const[o,u,x,C,h]=e;return n==="* * * * *"?"Every minute":o!=="*"&&u==="*"&&x==="*"&&C==="*"&&h==="*"?`Every hour at minute ${o}`:o!=="*"&&u!=="*"&&x==="*"&&C==="*"&&h==="*"?`Every day at ${u.padStart(2,"0")}:${o.padStart(2,"0")}`:o!=="*"&&u!=="*"&&x==="*"&&C==="*"&&h!=="*"?`Every ${["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"][h]} at ${u.padStart(2,"0")}:${o.padStart(2,"0")}`:o!=="*"&&u!=="*"&&x!=="*"&&C==="*"&&h==="*"?`On day ${x} of every month at ${u.padStart(2,"0")}:${o.padStart(2,"0")}`:`Custom: ${n}`},D=n=>new Date(n).toLocaleString("en-US",{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"}),R=async()=>{try{f.value?await W(f.value.id,{function_id:f.value.function_id,cron:r.value.cron,timezone:r.value.timezone,enabled:r.value.enabled}):await ne(r.value.function_name,{cron:r.value.cron,timezone:r.value.timezone,enabled:r.value.enabled}),await _(),z()}catch(n){console.error("Failed to save schedule",n),P.notify({title:"Failed to save schedule",danger:!0})}},H=n=>{f.value=n,r.value={function_name:n.function_name,cron:n.cron_expression,timezone:n.timezone||"UTC",enabled:n.enabled},w.value=!0},Y=async n=>{try{await W(n.id,{function_id:n.function_id,enabled:!n.enabled}),await _()}catch(e){console.error("Failed to toggle schedule",e)}},G=async n=>{if(await P.ask({title:"Delete schedule?",message:`Cron schedule for "${n.function_name}" will be removed.`,confirmLabel:"Delete",danger:!0}))try{await oe(n.id,n.function_id),await _()}catch(o){console.error("Failed to delete schedule",o)}},z=()=>{w.value=!1,f.value=null,r.value={function_name:"",cron:"0 0 * * *",timezone:k,enabled:!0},d.value={frequency:"day",minute:0,hour:0,dayOfWeek:1,dayOfMonth:1},g.value="simple"};return Z(()=>{_(),B(),v()}),(n,e)=>(l(),a("div",ie,[t("div",ce,[e[11]||(e[11]=t("div",null,[t("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Scheduled Jobs "),t("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"}," Cron-driven triggers that fire any deployed function on a schedule. Use them for periodic cleanup, daily reports, polling external APIs, anything you'd normally pin to a server's crontab. Orva runs the schedule, captures stdout/stderr, and surfaces failures in the activity feed. ")],-1)),c(U,{onClick:e[0]||(e[0]=o=>w.value=!0)},{default:F(()=>[c(i(ae),{class:"w-4 h-4"}),e[10]||(e[10]=b(" New Schedule ",-1))]),_:1})]),t("div",me,[t("table",fe,[e[14]||(e[14]=t("thead",{class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},[t("tr",null,[t("th",{class:"px-6 py-3 font-medium"}," Function "),t("th",{class:"px-6 py-3 font-medium"}," Schedule "),t("th",{class:"px-6 py-3 font-medium hidden sm:table-cell"}," Status "),t("th",{class:"px-6 py-3 font-medium hidden md:table-cell"}," Last Run "),t("th",{class:"px-6 py-3 font-medium hidden lg:table-cell"}," Next Run "),t("th",{class:"px-6 py-3 font-medium text-right"}," Actions ")])],-1)),t("tbody",pe,[(l(!0),a(S,null,$(T.value,o=>(l(),a("tr",{key:o.id,class:"hover:bg-surface-hover transition-colors"},[t("td",xe,s(o.function_name),1),t("td",be,[t("div",ye,[t("span",ve,s(o.cron_expression),1),t("span",ge,[b(s(E(o.cron_expression))+" ",1),t("span",he,"· "+s(o.timezone||"UTC"),1)])])]),t("td",ke,[t("span",{class:L(["inline-flex items-center px-2 py-0.5 rounded text-xs font-medium",o.enabled?"bg-success/10 text-success border border-success/30":"bg-foreground-muted/10 text-foreground-muted border border-foreground-muted/30"])},s(o.enabled?"Active":"Paused"),3)]),t("td",we,s(o.last_run_at?D(o.last_run_at):i(I)),1),t("td",_e,s(o.next_run_at?D(o.next_run_at):i(I)),1),t("td",Ce,[t("div",Se,[c(V,{icon:o.enabled?i(ue):i(re),title:o.enabled?"Pause":"Resume",onClick:u=>Y(o)},null,8,["icon","title","onClick"]),c(V,{icon:i(se),title:"Edit",onClick:u=>H(o)},null,8,["icon","onClick"]),c(V,{icon:i(de),variant:"danger",title:"Delete",onClick:u=>G(o)},null,8,["icon","onClick"])])])]))),128)),T.value.length===0?(l(),a("tr",$e,[t("td",Me,[c(i(le),{class:"w-12 h-12 text-foreground-muted mx-auto mb-3 opacity-30"}),e[12]||(e[12]=t("p",{class:"text-foreground-muted"}," No scheduled jobs yet. ",-1)),e[13]||(e[13]=t("p",{class:"text-foreground-muted text-xs mt-1"}," Create your first schedule to automate function execution. ",-1))])])):p("",!0)])])]),w.value?(l(),a("div",Ae,[t("div",Te,[t("div",Ee,[t("h2",ze,s(f.value?"Edit Schedule":"Create Schedule"),1),t("button",{class:"text-foreground-muted hover:text-foreground",onClick:z},[c(i(Q),{class:"w-5 h-5"})])]),t("div",Fe,[t("div",null,[e[16]||(e[16]=t("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-2"},"Function",-1)),m(t("select",{"onUpdate:modelValue":e[1]||(e[1]=o=>r.value.function_name=o),class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary",disabled:!!f.value},[e[15]||(e[15]=t("option",{value:""}," Select a function ",-1)),(l(!0),a(S,null,$(q.value,o=>(l(),a("option",{key:o.name,value:o.name},s(o.name)+" ("+s(o.runtime)+") ",9,Ve))),128))],8,Ue),[[M,r.value.function_name]])]),t("div",null,[e[17]||(e[17]=t("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-2"},"Schedule Type",-1)),t("div",Oe,[(l(),a(S,null,$(["simple","advanced"],o=>t("button",{key:o,class:L(["flex-1 py-2 px-3 text-sm font-medium rounded transition-colors",g.value===o?"bg-primary text-primary-foreground shadow-sm":"text-foreground-muted hover:text-foreground"]),onClick:u=>g.value=o},s(o==="simple"?"Natural Language":"Cron Expression"),11,Pe)),64))])]),g.value==="simple"?(l(),a("div",qe,[t("div",De,[t("div",null,[e[19]||(e[19]=t("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"Frequency",-1)),m(t("select",{"onUpdate:modelValue":e[2]||(e[2]=o=>d.value.frequency=o),class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary",onChange:v},[...e[18]||(e[18]=[N('',5)])],544),[[M,d.value.frequency]])]),["hour","day","week","month"].includes(d.value.frequency)?(l(),a("div",Ne,[e[20]||(e[20]=t("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"At Minute",-1)),m(t("input",{"onUpdate:modelValue":e[3]||(e[3]=o=>d.value.minute=o),type:"number",min:"0",max:"59",class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary",onInput:v},null,544),[[A,d.value.minute,void 0,{number:!0}]])])):p("",!0),["day","week","month"].includes(d.value.frequency)?(l(),a("div",Le,[e[21]||(e[21]=t("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"At Hour",-1)),m(t("input",{"onUpdate:modelValue":e[4]||(e[4]=o=>d.value.hour=o),type:"number",min:"0",max:"23",class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary",onInput:v},null,544),[[A,d.value.hour,void 0,{number:!0}]])])):p("",!0),d.value.frequency==="week"?(l(),a("div",We,[e[23]||(e[23]=t("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"Day of Week",-1)),m(t("select",{"onUpdate:modelValue":e[5]||(e[5]=o=>d.value.dayOfWeek=o),class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary",onChange:v},[...e[22]||(e[22]=[N('',7)])],544),[[M,d.value.dayOfWeek]])])):p("",!0),d.value.frequency==="month"?(l(),a("div",Ie,[e[24]||(e[24]=t("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"Day of Month",-1)),m(t("input",{"onUpdate:modelValue":e[6]||(e[6]=o=>d.value.dayOfMonth=o),type:"number",min:"1",max:"31",class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary",onInput:v},null,544),[[A,d.value.dayOfMonth,void 0,{number:!0}]])])):p("",!0)]),t("div",Je,[e[25]||(e[25]=t("div",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide mb-2"}," Generated Expression ",-1)),t("div",Be,s(r.value.cron),1),t("div",Re,s(E(r.value.cron)),1)])])):p("",!0),g.value==="advanced"?(l(),a("div",He,[t("div",null,[e[26]||(e[26]=t("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"Cron Expression",-1)),m(t("input",{"onUpdate:modelValue":e[7]||(e[7]=o=>r.value.cron=o),placeholder:"* * * * *",class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm font-mono text-foreground focus:outline-none focus:ring-2 focus:ring-primary"},null,512),[[A,r.value.cron]]),e[27]||(e[27]=t("p",{class:"text-xs text-foreground-muted mt-1.5"}," Format: minute hour day month weekday ",-1))]),t("div",Ye,[e[28]||(e[28]=t("div",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide mb-2"}," Preview ",-1)),t("div",Ge,s(E(r.value.cron)),1)])])):p("",!0),t("div",null,[e[33]||(e[33]=t("label",{class:"block text-xs font-medium text-foreground-muted uppercase tracking-wide mb-1.5"}," Timezone ",-1)),m(t("select",{"onUpdate:modelValue":e[8]||(e[8]=o=>r.value.timezone=o),class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:border-white"},[(l(!0),a(S,null,$(i(J),o=>(l(),a("option",{key:o,value:o},s(o)+s(o===i(k)?" (your browser)":""),9,Ke))),128))],512),[[M,r.value.timezone]]),t("div",Xe,[e[29]||(e[29]=b(" The cron expression is interpreted in this zone (e.g. ",-1)),e[30]||(e[30]=t("code",{class:"bg-surface px-1 rounded"},"0 9 * * *",-1)),e[31]||(e[31]=b(" with timezone ",-1)),t("code",Ze,s(r.value.timezone),1),e[32]||(e[32]=b(" fires at 9 AM local time every day. ",-1))])]),t("div",Qe,[m(t("input",{id:"enabled-toggle","onUpdate:modelValue":e[9]||(e[9]=o=>r.value.enabled=o),type:"checkbox",class:"w-4 h-4 text-primary bg-background border-border rounded focus:ring-primary focus:ring-2"},null,512),[[j,r.value.enabled]]),e[34]||(e[34]=t("label",{for:"enabled-toggle",class:"text-sm font-medium text-foreground cursor-pointer"}," Enable schedule immediately ",-1))])]),t("div",je,[c(U,{variant:"ghost",onClick:z},{default:F(()=>[...e[35]||(e[35]=[b(" Cancel ",-1)])]),_:1}),c(U,{disabled:!r.value.function_name||!r.value.cron,onClick:R},{default:F(()=>[b(s(f.value?"Update":"Create")+" Schedule ",1)]),_:1},8,["disabled"])])])])):p("",!0)]))}};export{lt as default}; diff --git a/backend/internal/server/ui_dist/assets/CronJobs-Uo-3xXwr.js b/backend/internal/server/ui_dist/assets/CronJobs-Uo-3xXwr.js new file mode 100644 index 0000000..57ac622 --- /dev/null +++ b/backend/internal/server/ui_dist/assets/CronJobs-Uo-3xXwr.js @@ -0,0 +1 @@ +import{c as X,au as oe,G as ne,o as se,a as l,b as e,d as c,h as _,_ as O,F as C,p as S,f as a,g as x,av as re,a0 as ae,r as v,j as d,k as m,t as r,s as q,n as J,K as R,e as f,V as F,v as U,ad as de,aw as G,ax as le,ay as ie}from"./index-CmY58qNN.js";import{E as P}from"./format-CsU4_SPu.js";import{_ as y}from"./IconButton-i2ilFBbf.js";import{_ as ue}from"./Modal-zN5wBHcp.js";import{C as V}from"./clock-CbnKorJ0.js";import{C as H}from"./circle-check-Z_CRCWca.js";import{P as K}from"./play-DZQVXk9D.js";import{S as Y}from"./square-pen-Cofsy7gY.js";import{T as Z}from"./trash-2-BJzRpJ7Y.js";const ce=X("circle-plus",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M8 12h8",key:"1wcyev"}],["path",{d:"M12 8v8",key:"napkw2"}]]);const Q=X("pause",[["rect",{x:"14",y:"3",width:"5",height:"18",rx:"1",key:"kaeet6"}],["rect",{x:"5",y:"3",width:"5",height:"18",rx:"1",key:"1wsw3u"}]]),me={class:"space-y-6"},fe={class:"flex items-center justify-between"},pe={class:"bg-background border border-border rounded-lg overflow-x-auto"},xe={class:"sm:hidden divide-y divide-border"},ge={class:"flex items-start justify-between gap-2"},ve={class:"min-w-0 flex-1"},be={class:"flex items-center gap-2 flex-wrap"},ye={class:"font-medium text-foreground truncate"},he={class:"mt-1 text-[11px] text-foreground font-mono break-all"},ke={class:"mt-0.5 text-[11px] text-foreground-muted"},we={class:"text-foreground-muted/70"},_e={class:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-foreground-muted"},Ce={class:"flex items-center gap-1 shrink-0"},Se={key:0,class:"px-6 py-12 text-center"},$e={class:"hidden sm:table w-full text-sm text-left"},Ae={class:"divide-y divide-border"},Me={class:"px-6 py-4 font-medium text-foreground max-w-[16rem] truncate"},Te={class:"px-6 py-4"},Ee={class:"flex flex-col gap-1"},ze={class:"text-foreground font-mono text-xs break-all"},Fe={class:"text-foreground-muted text-[10px]"},Ue={class:"text-foreground-muted/70"},Pe={class:"px-6 py-4 hidden sm:table-cell"},Ve={class:"px-6 py-4 text-foreground-muted text-xs hidden md:table-cell"},De={class:"px-6 py-4 text-foreground-muted text-xs hidden lg:table-cell"},Oe={class:"px-6 py-4 text-right"},qe={class:"inline-flex items-center gap-1"},Ne={key:0},Le={colspan:"6",class:"px-6 py-12 text-center"},We={class:"space-y-5"},Ie=["disabled"],Be=["value"],Je={class:"flex gap-2 bg-background rounded-lg p-1 border border-border"},Re=["onClick"],Ge={key:0,class:"space-y-4"},He={class:"grid grid-cols-3 gap-3"},Ke={key:0},Ye={key:1},Ze={key:2},Qe={key:3},Xe={class:"bg-background border border-border rounded-lg p-4"},je={class:"font-mono text-sm text-foreground"},et={class:"text-xs text-foreground-muted mt-1"},tt={key:1,class:"space-y-3"},ot={class:"bg-background border border-border rounded-lg p-4"},nt={class:"text-xs text-foreground"},st=["value"],rt={class:"text-xs text-foreground-muted mt-1.5"},at={class:"bg-surface px-1 rounded"},dt={class:"flex items-center gap-3"},yt={__name:"CronJobs",setup(lt){const $=oe(),j=(()=>{const n=["UTC","America/Los_Angeles","America/New_York","America/Chicago","America/Denver","America/Sao_Paulo","Europe/London","Europe/Berlin","Europe/Paris","Europe/Moscow","Africa/Lagos","Africa/Cairo","Africa/Johannesburg","Asia/Dubai","Asia/Kolkata","Asia/Singapore","Asia/Shanghai","Asia/Tokyo","Australia/Sydney","Pacific/Auckland"];return[...new Set([$,...n])]})(),N=ne(),h=v([]),L=v([]),A=v(!1),p=v(null),k=v("simple"),s=v({function_name:"",cron:"0 0 * * *",timezone:$,enabled:!0}),i=v({frequency:"day",minute:0,hour:0,dayOfWeek:1,dayOfMonth:1}),M=async()=>{try{const n=await re();h.value=n.data.schedules||[]}catch(n){console.error("Failed to load cron jobs",n)}},ee=async()=>{try{const n=await ae();L.value=n.data.functions||[]}catch(n){console.error("Failed to load functions",n)}},b=()=>{const{frequency:n,minute:t,hour:o,dayOfWeek:u,dayOfMonth:g}=i.value;switch(n){case"minute":s.value.cron="* * * * *";break;case"hour":s.value.cron=`${t} * * * *`;break;case"day":s.value.cron=`${t} ${o} * * *`;break;case"week":s.value.cron=`${t} ${o} * * ${u}`;break;case"month":s.value.cron=`${t} ${o} ${g} * *`;break}},T=n=>{if(!n)return"Invalid expression";const t=n.trim().split(/\s+/);if(t.length!==5)return"Invalid format (use 5 fields)";const[o,u,g,z,w]=t;return n==="* * * * *"?"Every minute":o!=="*"&&u==="*"&&g==="*"&&z==="*"&&w==="*"?`Every hour at minute ${o}`:o!=="*"&&u!=="*"&&g==="*"&&z==="*"&&w==="*"?`Every day at ${u.padStart(2,"0")}:${o.padStart(2,"0")}`:o!=="*"&&u!=="*"&&g==="*"&&z==="*"&&w!=="*"?`Every ${["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"][w]} at ${u.padStart(2,"0")}:${o.padStart(2,"0")}`:o!=="*"&&u!=="*"&&g!=="*"&&z==="*"&&w==="*"?`On day ${g} of every month at ${u.padStart(2,"0")}:${o.padStart(2,"0")}`:`Custom: ${n}`},E=n=>new Date(n).toLocaleString("en-US",{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"}),te=async()=>{try{p.value?await G(p.value.id,{function_id:p.value.function_id,cron:s.value.cron,timezone:s.value.timezone,enabled:s.value.enabled}):await ie(s.value.function_name,{cron:s.value.cron,timezone:s.value.timezone,enabled:s.value.enabled}),await M(),D()}catch(n){console.error("Failed to save schedule",n),N.notify({title:"Failed to save schedule",danger:!0})}},W=n=>{p.value=n,s.value={function_name:n.function_name,cron:n.cron_expression,timezone:n.timezone||"UTC",enabled:n.enabled},A.value=!0},I=async n=>{try{await G(n.id,{function_id:n.function_id,enabled:!n.enabled}),await M()}catch(t){console.error("Failed to toggle schedule",t)}},B=async n=>{if(await N.ask({title:"Delete schedule?",message:`Cron schedule for "${n.function_name}" will be removed.`,confirmLabel:"Delete",danger:!0}))try{await le(n.id,n.function_id),await M()}catch(o){console.error("Failed to delete schedule",o)}},D=()=>{A.value=!1,p.value=null,s.value={function_name:"",cron:"0 0 * * *",timezone:$,enabled:!0},i.value={frequency:"day",minute:0,hour:0,dayOfWeek:1,dayOfMonth:1},k.value="simple"};return se(()=>{M(),ee(),b()}),(n,t)=>(d(),l("div",me,[e("div",fe,[t[12]||(t[12]=e("div",null,[e("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Scheduled Jobs "),e("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"}," Cron-driven triggers that fire any deployed function on a schedule. Use them for periodic cleanup, daily reports, polling external APIs, anything you'd normally pin to a server's crontab. Orva runs the schedule, captures stdout/stderr, and surfaces failures in the activity feed. ")],-1)),c(O,{onClick:t[0]||(t[0]=o=>A.value=!0)},{default:_(()=>[c(a(ce),{class:"w-4 h-4"}),t[11]||(t[11]=m(" New Schedule ",-1))]),_:1})]),e("div",pe,[e("ul",xe,[(d(!0),l(C,null,S(h.value,o=>(d(),l("li",{key:o.id,class:"px-4 py-3"},[e("div",ge,[e("div",ve,[e("div",be,[e("span",ye,r(o.function_name),1),e("span",{class:q(["inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-medium border",o.enabled?"bg-success-tint text-success-fg border-success-ring":"bg-warning-tint text-warning-fg border-warning-ring"])},[(d(),J(R(o.enabled?a(H):a(V)),{class:"h-3 w-3 shrink-0","aria-hidden":"true"})),m(" "+r(o.enabled?"Active":"Paused"),1)],2)]),e("div",he,r(o.cron_expression),1),e("div",ke,[m(r(T(o.cron_expression))+" ",1),e("span",we,"· "+r(o.timezone||"UTC"),1)]),e("div",_e,[e("span",null,"last "+r(o.last_run_at?E(o.last_run_at):a(P)),1),e("span",null,"next "+r(o.next_run_at?E(o.next_run_at):a(P)),1)])]),e("div",Ce,[c(y,{icon:o.enabled?a(Q):a(K),title:o.enabled?"Pause":"Resume",onClick:u=>I(o)},null,8,["icon","title","onClick"]),c(y,{icon:a(Y),title:"Edit",onClick:u=>W(o)},null,8,["icon","onClick"]),c(y,{icon:a(Z),variant:"danger",title:"Delete",onClick:u=>B(o)},null,8,["icon","onClick"])])])]))),128)),h.value.length===0?(d(),l("li",Se,[c(a(V),{class:"w-12 h-12 text-foreground-muted mx-auto mb-3 opacity-60"}),t[13]||(t[13]=e("p",{class:"text-foreground-muted"}," No scheduled jobs yet. ",-1)),t[14]||(t[14]=e("p",{class:"text-foreground-muted text-xs mt-1"}," Create your first schedule to automate function execution. ",-1))])):x("",!0)]),e("table",$e,[t[17]||(t[17]=e("thead",{class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},[e("tr",null,[e("th",{class:"px-6 py-3 font-medium"}," Function "),e("th",{class:"px-6 py-3 font-medium"}," Schedule "),e("th",{class:"px-6 py-3 font-medium hidden sm:table-cell"}," Status "),e("th",{class:"px-6 py-3 font-medium hidden md:table-cell"}," Last Run "),e("th",{class:"px-6 py-3 font-medium hidden lg:table-cell"}," Next Run "),e("th",{class:"px-6 py-3 font-medium text-right"}," Actions ")])],-1)),e("tbody",Ae,[(d(!0),l(C,null,S(h.value,o=>(d(),l("tr",{key:o.id,class:"hover:bg-surface-hover transition-colors"},[e("td",Me,r(o.function_name),1),e("td",Te,[e("div",Ee,[e("span",ze,r(o.cron_expression),1),e("span",Fe,[m(r(T(o.cron_expression))+" ",1),e("span",Ue,"· "+r(o.timezone||"UTC"),1)])])]),e("td",Pe,[e("span",{class:q(["inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium border",o.enabled?"bg-success-tint text-success-fg border-success-ring":"bg-warning-tint text-warning-fg border-warning-ring"])},[(d(),J(R(o.enabled?a(H):a(V)),{class:"h-3 w-3 shrink-0","aria-hidden":"true"})),m(" "+r(o.enabled?"Active":"Paused"),1)],2)]),e("td",Ve,r(o.last_run_at?E(o.last_run_at):a(P)),1),e("td",De,r(o.next_run_at?E(o.next_run_at):a(P)),1),e("td",Oe,[e("div",qe,[c(y,{icon:o.enabled?a(Q):a(K),title:o.enabled?"Pause":"Resume",onClick:u=>I(o)},null,8,["icon","title","onClick"]),c(y,{icon:a(Y),title:"Edit",onClick:u=>W(o)},null,8,["icon","onClick"]),c(y,{icon:a(Z),variant:"danger",title:"Delete",onClick:u=>B(o)},null,8,["icon","onClick"])])])]))),128)),h.value.length===0?(d(),l("tr",Ne,[e("td",Le,[c(a(V),{class:"w-12 h-12 text-foreground-muted mx-auto mb-3 opacity-60"}),t[15]||(t[15]=e("p",{class:"text-foreground-muted"}," No scheduled jobs yet. ",-1)),t[16]||(t[16]=e("p",{class:"text-foreground-muted text-xs mt-1"}," Create your first schedule to automate function execution. ",-1))])])):x("",!0)])])]),c(ue,{"model-value":A.value,title:p.value?"Edit Schedule":"Create Schedule",size:"lg","onUpdate:modelValue":t[10]||(t[10]=o=>{o||D()})},{footer:_(()=>[c(O,{variant:"ghost",onClick:D},{default:_(()=>[...t[38]||(t[38]=[m(" Cancel ",-1)])]),_:1}),c(O,{disabled:!s.value.function_name||!s.value.cron,onClick:te},{default:_(()=>[m(r(p.value?"Update":"Create")+" Schedule ",1)]),_:1},8,["disabled"])]),default:_(()=>[e("div",We,[e("div",null,[t[19]||(t[19]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-2"},"Function",-1)),f(e("select",{"onUpdate:modelValue":t[1]||(t[1]=o=>s.value.function_name=o),class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",disabled:!!p.value},[t[18]||(t[18]=e("option",{value:""}," Select a function ",-1)),(d(!0),l(C,null,S(L.value,o=>(d(),l("option",{key:o.name,value:o.name},r(o.name)+" ("+r(o.runtime)+") ",9,Be))),128))],8,Ie),[[F,s.value.function_name]])]),e("div",null,[t[20]||(t[20]=e("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-2"},"Schedule Type",-1)),e("div",Je,[(d(),l(C,null,S(["simple","advanced"],o=>e("button",{key:o,class:q(["flex-1 py-2 px-3 text-sm font-medium rounded transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background",k.value===o?"bg-primary text-primary-foreground shadow-sm":"text-foreground-muted hover:text-foreground"]),onClick:u=>k.value=o},r(o==="simple"?"Natural Language":"Cron Expression"),11,Re)),64))])]),k.value==="simple"?(d(),l("div",Ge,[e("div",He,[e("div",null,[t[22]||(t[22]=e("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"Frequency",-1)),f(e("select",{"onUpdate:modelValue":t[2]||(t[2]=o=>i.value.frequency=o),class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onChange:b},[...t[21]||(t[21]=[e("option",{value:"minute"}," Every Minute ",-1),e("option",{value:"hour"}," Hourly ",-1),e("option",{value:"day"}," Daily ",-1),e("option",{value:"week"}," Weekly ",-1),e("option",{value:"month"}," Monthly ",-1)])],544),[[F,i.value.frequency]])]),["hour","day","week","month"].includes(i.value.frequency)?(d(),l("div",Ke,[t[23]||(t[23]=e("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"At Minute",-1)),f(e("input",{"onUpdate:modelValue":t[3]||(t[3]=o=>i.value.minute=o),type:"number",min:"0",max:"59",class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onInput:b},null,544),[[U,i.value.minute,void 0,{number:!0}]])])):x("",!0),["day","week","month"].includes(i.value.frequency)?(d(),l("div",Ye,[t[24]||(t[24]=e("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"At Hour",-1)),f(e("input",{"onUpdate:modelValue":t[4]||(t[4]=o=>i.value.hour=o),type:"number",min:"0",max:"23",class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onInput:b},null,544),[[U,i.value.hour,void 0,{number:!0}]])])):x("",!0),i.value.frequency==="week"?(d(),l("div",Ze,[t[26]||(t[26]=e("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"Day of Week",-1)),f(e("select",{"onUpdate:modelValue":t[5]||(t[5]=o=>i.value.dayOfWeek=o),class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onChange:b},[...t[25]||(t[25]=[e("option",{value:"0"}," Sunday ",-1),e("option",{value:"1"}," Monday ",-1),e("option",{value:"2"}," Tuesday ",-1),e("option",{value:"3"}," Wednesday ",-1),e("option",{value:"4"}," Thursday ",-1),e("option",{value:"5"}," Friday ",-1),e("option",{value:"6"}," Saturday ",-1)])],544),[[F,i.value.dayOfWeek]])])):x("",!0),i.value.frequency==="month"?(d(),l("div",Qe,[t[27]||(t[27]=e("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"Day of Month",-1)),f(e("input",{"onUpdate:modelValue":t[6]||(t[6]=o=>i.value.dayOfMonth=o),type:"number",min:"1",max:"31",class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onInput:b},null,544),[[U,i.value.dayOfMonth,void 0,{number:!0}]])])):x("",!0)]),e("div",Xe,[t[28]||(t[28]=e("div",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide mb-2"}," Generated Expression ",-1)),e("div",je,r(s.value.cron),1),e("div",et,r(T(s.value.cron)),1)])])):x("",!0),k.value==="advanced"?(d(),l("div",tt,[e("div",null,[t[29]||(t[29]=e("label",{class:"text-xs font-medium text-foreground-muted block mb-1.5"},"Cron Expression",-1)),f(e("input",{"onUpdate:modelValue":t[7]||(t[7]=o=>s.value.cron=o),placeholder:"* * * * *",class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm font-mono text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white"},null,512),[[U,s.value.cron]]),t[30]||(t[30]=e("p",{class:"text-xs text-foreground-muted mt-1.5"}," Format: minute hour day month weekday ",-1))]),e("div",ot,[t[31]||(t[31]=e("div",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide mb-2"}," Preview ",-1)),e("div",nt,r(T(s.value.cron)),1)])])):x("",!0),e("div",null,[t[36]||(t[36]=e("label",{class:"block text-xs font-medium text-foreground-muted uppercase tracking-wide mb-1.5"}," Timezone ",-1)),f(e("select",{"onUpdate:modelValue":t[8]||(t[8]=o=>s.value.timezone=o),class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:border-white"},[(d(!0),l(C,null,S(a(j),o=>(d(),l("option",{key:o,value:o},r(o)+r(o===a($)?" (your browser)":""),9,st))),128))],512),[[F,s.value.timezone]]),e("div",rt,[t[32]||(t[32]=m(" The cron expression is interpreted in this zone (e.g. ",-1)),t[33]||(t[33]=e("code",{class:"bg-surface px-1 rounded"},"0 9 * * *",-1)),t[34]||(t[34]=m(" with timezone ",-1)),e("code",at,r(s.value.timezone),1),t[35]||(t[35]=m(" fires at 9 AM local time every day. ",-1))])]),e("div",dt,[f(e("input",{id:"enabled-toggle","onUpdate:modelValue":t[9]||(t[9]=o=>s.value.enabled=o),type:"checkbox",class:"w-4 h-4 text-primary bg-background border-border rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background"},null,512),[[de,s.value.enabled]]),t[37]||(t[37]=e("label",{for:"enabled-toggle",class:"text-sm font-medium text-foreground cursor-pointer"}," Enable schedule immediately ",-1))])])]),_:1},8,["model-value","title"])]))}};export{yt as default}; diff --git a/backend/internal/server/ui_dist/assets/Dashboard-BWI_asI9.js b/backend/internal/server/ui_dist/assets/Dashboard-DfJlGjKT.js similarity index 99% rename from backend/internal/server/ui_dist/assets/Dashboard-BWI_asI9.js rename to backend/internal/server/ui_dist/assets/Dashboard-DfJlGjKT.js index b577a04..8422985 100644 --- a/backend/internal/server/ui_dist/assets/Dashboard-BWI_asI9.js +++ b/backend/internal/server/ui_dist/assets/Dashboard-DfJlGjKT.js @@ -1 +1 @@ -import{E as y}from"./format-CsU4_SPu.js";import{c as M,x as E,o as H,y as I,a as c,b as t,d as r,f as u,B as O,A as V,t as a,k as i,g as F,F as z,p as W,h as G,_ as Y,q as h,j as m,P as J,z as l}from"./index-WXhXpu06.js";const K=M("snowflake",[["path",{d:"m10 20-1.25-2.5L6 18",key:"18frcb"}],["path",{d:"M10 4 8.75 6.5 6 6",key:"7mghy3"}],["path",{d:"m14 20 1.25-2.5L18 18",key:"1chtki"}],["path",{d:"m14 4 1.25 2.5L18 6",key:"1b4wsy"}],["path",{d:"m17 21-3-6h-4",key:"15hhxa"}],["path",{d:"m17 3-3 6 1.5 3",key:"11697g"}],["path",{d:"M2 12h6.5L10 9",key:"kv9z4n"}],["path",{d:"m20 10-1.5 2 1.5 2",key:"1swlpi"}],["path",{d:"M22 12h-6.5L14 15",key:"1mxi28"}],["path",{d:"m4 10 1.5 2L4 14",key:"k9enpj"}],["path",{d:"m7 21 3-6-1.5-3",key:"j8hb9u"}],["path",{d:"m7 3 3 6h4",key:"1otusx"}]]);const Q=M("trending-up",[["path",{d:"M16 7h6v6",key:"box55l"}],["path",{d:"m22 7-8.5 8.5-5-5L2 17",key:"1t1m79"}]]),X={class:"space-y-6"},Z={class:"grid grid-cols-2 md:grid-cols-4 gap-4"},tt={class:"grid grid-cols-1 lg:grid-cols-3 gap-4"},et={class:"bg-background border border-border rounded-lg p-5 lg:col-span-1"},st={class:"bg-background border border-border rounded-lg p-5 lg:col-span-2 space-y-5"},ot={class:"grid grid-cols-2 gap-4 text-sm"},lt={class:"text-lg font-mono text-white mt-0.5"},nt={class:"text-lg font-mono text-white mt-0.5"},at={class:"text-foreground-muted text-sm"},rt={class:"text-[11px] text-foreground-muted mt-0.5"},dt={class:"space-y-2"},it={class:"flex flex-wrap items-center gap-x-4 gap-y-1 text-[11px] text-foreground-muted"},ut={class:"flex items-center gap-1.5"},ct={class:"flex items-center gap-1.5"},mt={class:"grid grid-cols-1 md:grid-cols-2 gap-4"},vt={class:"bg-background border border-border rounded-lg p-5 space-y-3"},pt={class:"grid grid-cols-3 gap-3"},gt={key:0,class:"text-xs text-red-400 flex items-center gap-1.5 pt-1"},xt={class:"bg-background border border-border rounded-lg p-5 space-y-3"},bt={class:"grid grid-cols-3 gap-3"},ht={key:0},ft={class:"flex items-baseline justify-between mb-3"},_t={class:"text-xs font-bold text-white uppercase tracking-wider"},wt={class:"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4"},yt={class:"flex items-start justify-between gap-2"},kt={class:"min-w-0"},St={class:"text-sm font-medium text-white truncate"},$t={class:"text-[10px] text-foreground-muted font-mono truncate"},Ft={class:"text-right shrink-0"},Mt={class:"text-xs font-mono text-white"},Bt={class:"grid grid-cols-3 gap-2"},Nt={class:"border-t border-border pt-3 grid grid-cols-2 gap-3 text-[11px]"},Ct={class:"font-mono text-white"},Lt={class:"font-mono text-white"},jt={class:"font-mono text-white",title:"Average memory used per invocation vs allocated limit"},At={class:"text-foreground-muted"},Rt={class:"font-mono text-white",title:"Average CPU cores consumed per invocation vs allocated"},Pt={key:0,class:"text-foreground-muted"},qt={key:1,class:"bg-background border border-border rounded-lg p-8 text-center space-y-4"},Et={__name:"Dashboard",setup(Tt){const p=E(),n=h(()=>p.metrics||{}),B=s=>p.poolHistory[s]||[],N=s=>s==null?y:`${s.toFixed(1)}%`,C=s=>s==null?"0":s.toFixed(1),f=s=>{const e=s||0;return e>=1024?`${(e/1024).toFixed(1)} GB`:`${Math.round(e)} MB`},_=s=>{const e=Number(s)||0;return e>=1e6?`${(e/1e6).toFixed(1)}M`:e>=1e3?`${(e/1e3).toFixed(1)}k`:String(e)},g=h(()=>n.value.host?.mem_total_mb??0),x=h(()=>n.value.host?.mem_reserved_mb??0),S=h(()=>Math.max(0,g.value-x.value)),L=h(()=>g.value>0?x.value/g.value*100:0);H(()=>p.connect()),I(()=>p.disconnect());const w={props:{label:String,value:[String,Number],icon:Object,hint:String},setup(s){return()=>l("div",{class:"bg-background border border-border rounded-lg p-5 flex flex-col h-full hover:border-primary/50 transition-colors group"},[l("div",{class:"flex items-center justify-between mb-3"},[l("span",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide"},s.label),s.icon?l(s.icon,{class:"w-4 h-4 text-foreground-muted group-hover:text-primary"}):null]),l("div",{class:"text-2xl font-mono text-foreground leading-none"},String(s.value)),s.hint?l("div",{class:"text-[11px] text-foreground-muted mt-auto pt-3 leading-snug"},s.hint):null])}},j={props:{p50:Number,p95:Number,p99:Number},setup(s){return()=>{const e=[{label:"p50",ms:s.p50,color:"bg-success/70"},{label:"p95",ms:s.p95,color:"bg-warning/70"},{label:"p99",ms:s.p99,color:"bg-danger/70"}],o=Math.max(s.p50||0,s.p95||0,s.p99||0,1);return l("div",{class:"space-y-2.5"},e.map(d=>{const b=d.ms==null?0:d.ms/o*100;return l("div",{class:"space-y-1"},[l("div",{class:"flex items-baseline justify-between text-[11px]"},[l("span",{class:"font-mono uppercase text-foreground-muted tracking-wider"},d.label),l("span",{class:"font-mono text-white"},d.ms==null?y:`${d.ms}ms`)]),l("div",{class:"h-1.5 bg-surface rounded overflow-hidden"},[l("div",{class:`h-full ${d.color} transition-[width] duration-500 ease-out`,style:{width:`${b.toFixed(1)}%`}})])])}))}}},v={props:{label:String,value:[String,Number],hint:String},setup(s){return()=>l("div",{class:"bg-surface border border-border rounded p-3 flex flex-col h-full"},[l("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted"},s.label),l("div",{class:"text-lg font-mono text-white mt-0.5"},String(s.value??0)),s.hint?l("div",{class:"text-[10px] text-foreground-muted mt-auto pt-1.5 leading-snug"},s.hint):null])}},k={props:{label:String,value:[String,Number],hint:String},setup(s){return()=>l("div",{class:"bg-surface border border-border rounded p-2.5 flex flex-col h-full"},[l("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted"},s.label),l("div",{class:"text-base font-mono text-white mt-0.5 leading-none"},String(s.value??0)),s.hint?l("div",{class:"text-[10px] text-foreground-muted mt-auto pt-1.5"},s.hint):null])}},A={props:{total:{type:Number,required:!0},segments:{type:Array,required:!0}},setup(s){return()=>{const e=s.total>0?s.total:1;return l("div",{class:"h-2.5 bg-surface rounded overflow-hidden flex",role:"img","aria-label":s.segments.map(o=>`${o.label}: ${o.value} of ${s.total}`).join("; ")},s.segments.map(o=>l("div",{class:`h-full ${o.color}`,style:{width:`${(o.value/e*100).toFixed(2)}%`},title:`${o.label}: ${o.value}`})))}}},R={props:{points:{type:Array,default:()=>[]}},setup(s){return()=>{const e=s.points||[];if(e.length<2)return l("div",{class:"h-8 flex items-center text-[10px] text-foreground-muted"},"(collecting samples…)");const o=Math.max(...e,1),d=100,b=32,P=d/(e.length-1),q=e.map((T,$)=>{const U=($*P).toFixed(2),D=(b-T/o*b).toFixed(2);return`${$===0?"M":"L"}${U},${D}`}).join(" ");return l("svg",{viewBox:`0 0 ${d} ${b}`,class:"w-full h-8 text-blue-400",preserveAspectRatio:"none"},[l("path",{d:q,fill:"none",stroke:"currentColor","stroke-width":"1.5"})])}}};return(s,e)=>(m(),c("div",X,[e[21]||(e[21]=t("div",null,[t("h1",{class:"text-xl font-semibold text-white tracking-tight"},"System Overview"),t("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"},"Live snapshot of what your platform is doing right now.")],-1)),t("div",Z,[r(w,{label:"Functions",hint:"Deployed in this workspace",value:u(p).functionsCount,icon:u(O)},null,8,["value","icon"]),r(w,{label:"In flight",hint:"Requests being handled right now",value:n.value.active_requests??0,icon:u(V)},null,8,["value","icon"]),r(w,{label:"Invocations",hint:"Total calls served since the platform started",value:_(n.value.totals?.invocations??0),icon:u(Q)},null,8,["value","icon"]),r(w,{label:"Cold starts",hint:"Calls that had to spawn a fresh sandbox",value:N(n.value.rates?.cold_start_pct),icon:u(K)},null,8,["value","icon"])]),t("div",tt,[t("div",et,[e[1]||(e[1]=t("div",{class:"mb-3"},[t("h2",{class:"text-xs font-bold text-white uppercase tracking-wider"}," Response time "),t("div",{class:"text-[11px] text-foreground-muted mt-1"}," How long calls take to come back. p99 is the worst-case 1-in-100. ")],-1)),r(j,{p50:n.value.latency_ms?.p50,p95:n.value.latency_ms?.p95,p99:n.value.latency_ms?.p99},null,8,["p50","p95","p99"])]),t("div",st,[e[7]||(e[7]=t("div",null,[t("h2",{class:"text-xs font-bold text-white uppercase tracking-wider"}," Host machine "),t("div",{class:"text-[11px] text-foreground-muted mt-1"}," The server Orva is running on, and how much of its RAM your warm sandboxes are holding. ")],-1)),t("div",ot,[t("div",null,[e[2]||(e[2]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted"},"CPU cores",-1)),t("div",lt,a(n.value.host?.num_cpu??"?"),1),e[3]||(e[3]=t("div",{class:"text-[11px] text-foreground-muted mt-0.5"}," available to functions on this host ",-1))]),t("div",null,[e[4]||(e[4]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted"},"Memory in use",-1)),t("div",nt,[i(a(f(x.value))+" ",1),t("span",at,"/ "+a(f(g.value)),1)]),t("div",rt,a(L.value.toFixed(1))+"% reserved by warm sandbox pools ",1)])]),t("div",dt,[r(A,{total:g.value,segments:[{label:"Reserved by warm pools",value:x.value,color:"bg-info/70"},{label:"Free",value:S.value,color:"bg-success/40"}]},null,8,["total","segments"]),t("div",it,[t("span",ut,[e[5]||(e[5]=t("span",{class:"w-2 h-2 rounded-full bg-info/70"},null,-1)),i(" "+a(f(x.value))+" held by warm sandboxes ready to serve ",1)]),t("span",ct,[e[6]||(e[6]=t("span",{class:"w-2 h-2 rounded-full bg-success/40"},null,-1)),i(" "+a(f(S.value))+" free for new pools or other workloads ",1)])])])])]),t("div",mt,[t("div",vt,[e[9]||(e[9]=t("div",null,[t("h2",{class:"text-xs font-bold text-white uppercase tracking-wider"}," Builds "),t("div",{class:"text-[11px] text-foreground-muted mt-1"}," Where deploys go: extracted, dependencies installed, then activated. ")],-1)),t("div",pt,[r(v,{label:"In queue",value:n.value.build_queue?.pending??0,hint:"waiting to start"},null,8,["value"]),r(v,{label:"Build workers",value:n.value.build_queue?.workers??0,hint:"parallel slots"},null,8,["value"]),r(v,{label:"Built so far",value:_(n.value.totals?.builds??0),hint:"lifetime total"},null,8,["value"])]),(n.value.totals?.build_errors??0)>0?(m(),c("div",gt,[e[8]||(e[8]=t("span",{class:"w-1.5 h-1.5 rounded-full bg-red-400"},null,-1)),i(" "+a(n.value.totals.build_errors)+" build"+a(n.value.totals.build_errors===1?" has":"s have")+" failed since start ",1)])):F("",!0)]),t("div",xt,[e[10]||(e[10]=t("div",null,[t("h2",{class:"text-xs font-bold text-white uppercase tracking-wider"}," Sandbox activity "),t("div",{class:"text-[11px] text-foreground-muted mt-1"}," Each invocation runs inside an isolated nsjail sandbox process. ")],-1)),t("div",bt,[r(v,{label:"Running now",value:n.value.sandbox?.active??0,hint:"serving a request"},null,8,["value"]),r(v,{label:"Reused",value:_(n.value.totals?.warm_hits??0),hint:"warm-pool hits"},null,8,["value"]),r(v,{label:"Spawned fresh",value:_(n.value.totals?.cold_starts??0),hint:"cold starts"},null,8,["value"])])])]),(n.value.pools||[]).length?(m(),c("div",ht,[t("div",ft,[t("div",null,[t("h2",_t," Warm pools ("+a(n.value.pools.length)+") ",1),e[11]||(e[11]=t("div",{class:"text-[11px] text-foreground-muted mt-1"}," One pool per active function. Sandboxes stay ready so the next call doesn't pay a cold start. ",-1))])]),t("div",wt,[(m(!0),c(z,null,W(n.value.pools,o=>(m(),c("div",{key:o.function_id,class:"bg-background border border-border rounded-lg p-4 space-y-3"},[t("div",yt,[t("div",kt,[t("div",St,a(o.function_name||o.function_id),1),t("div",$t,a(o.function_id),1)]),t("div",Ft,[e[13]||(e[13]=t("div",{class:"text-[10px] text-foreground-muted"},"Target / cap",-1)),t("div",Mt,[i(a(o.target)+" ",1),e[12]||(e[12]=t("span",{class:"text-foreground-muted"},"/",-1)),i(" "+a(o.dynamic_max),1)])])]),t("div",Bt,[r(k,{label:"Ready",value:o.idle,hint:"idle workers"},null,8,["value"]),r(k,{label:"Busy",value:o.busy,hint:"serving now"},null,8,["value"]),r(k,{label:"Calls / sec",value:C(o.rate_ewma),hint:"recent rate"},null,8,["value"])]),t("div",null,[r(R,{points:B(o.function_id)},null,8,["points"]),e[14]||(e[14]=t("div",{class:"text-[10px] text-foreground-muted mt-1"}," Recent calls per second (last 5 min) ",-1))]),t("div",Nt,[t("div",null,[e[15]||(e[15]=t("div",{class:"text-foreground-muted"},"Spawned · killed",-1)),t("div",Ct,a(o.spawned)+" · "+a(o.killed),1)]),t("div",null,[e[16]||(e[16]=t("div",{class:"text-foreground-muted"},"Avg latency",-1)),t("div",Lt,a(o.latency_ewma_ms?.toFixed?.(1)??0)+" ms",1)]),t("div",null,[e[17]||(e[17]=t("div",{class:"text-foreground-muted"},"Avg memory",-1)),t("div",jt,[i(a(o.mem_used_avg_mb>0?"~"+Math.round(o.mem_used_avg_mb):u(y))+" ",1),t("span",At,"/ "+a(o.mem_limit_mb)+" MB",1)])]),t("div",null,[e[18]||(e[18]=t("div",{class:"text-foreground-muted"},"Avg CPU",-1)),t("div",Rt,[i(a(o.cpu_frac_avg>0&&o.cpu_limit>0?(o.cpu_frac_avg*o.cpu_limit).toFixed(2):u(y))+" ",1),o.cpu_limit>0?(m(),c("span",Pt,"/ "+a(o.cpu_limit)+" CPU",1)):F("",!0)])])])]))),128))])])):(m(),c("div",qt,[e[20]||(e[20]=t("div",null,[t("div",{class:"text-sm text-white"},"No warm pools yet"),t("div",{class:"text-xs text-foreground-muted mt-1 max-w-prose mx-auto leading-body"}," Deploy your first function to see live worker pools, latency, and cold-start rate land in the tiles above. ")],-1)),t("div",null,[r(Y,{onClick:e[0]||(e[0]=o=>s.$router.push("/functions/new"))},{default:G(()=>[r(u(J),{class:"w-4 h-4"}),e[19]||(e[19]=i(" Deploy your first function ",-1))]),_:1})])]))]))}};export{Et as default}; +import{E as y}from"./format-CsU4_SPu.js";import{c as M,x as E,o as H,y as I,a as c,b as t,d as r,f as u,B as O,A as V,t as a,k as i,g as F,F as z,p as W,h as G,_ as Y,q as h,j as m,P as J,z as l}from"./index-CmY58qNN.js";const K=M("snowflake",[["path",{d:"m10 20-1.25-2.5L6 18",key:"18frcb"}],["path",{d:"M10 4 8.75 6.5 6 6",key:"7mghy3"}],["path",{d:"m14 20 1.25-2.5L18 18",key:"1chtki"}],["path",{d:"m14 4 1.25 2.5L18 6",key:"1b4wsy"}],["path",{d:"m17 21-3-6h-4",key:"15hhxa"}],["path",{d:"m17 3-3 6 1.5 3",key:"11697g"}],["path",{d:"M2 12h6.5L10 9",key:"kv9z4n"}],["path",{d:"m20 10-1.5 2 1.5 2",key:"1swlpi"}],["path",{d:"M22 12h-6.5L14 15",key:"1mxi28"}],["path",{d:"m4 10 1.5 2L4 14",key:"k9enpj"}],["path",{d:"m7 21 3-6-1.5-3",key:"j8hb9u"}],["path",{d:"m7 3 3 6h4",key:"1otusx"}]]);const Q=M("trending-up",[["path",{d:"M16 7h6v6",key:"box55l"}],["path",{d:"m22 7-8.5 8.5-5-5L2 17",key:"1t1m79"}]]),X={class:"space-y-6"},Z={class:"grid grid-cols-2 md:grid-cols-4 gap-4"},tt={class:"grid grid-cols-1 lg:grid-cols-3 gap-4"},et={class:"bg-background border border-border rounded-lg p-5 lg:col-span-1"},st={class:"bg-background border border-border rounded-lg p-5 lg:col-span-2 space-y-5"},ot={class:"grid grid-cols-2 gap-4 text-sm"},lt={class:"text-lg font-mono text-white mt-0.5"},nt={class:"text-lg font-mono text-white mt-0.5"},at={class:"text-foreground-muted text-sm"},rt={class:"text-[11px] text-foreground-muted mt-0.5"},dt={class:"space-y-2"},it={class:"flex flex-wrap items-center gap-x-4 gap-y-1 text-[11px] text-foreground-muted"},ut={class:"flex items-center gap-1.5"},ct={class:"flex items-center gap-1.5"},mt={class:"grid grid-cols-1 md:grid-cols-2 gap-4"},vt={class:"bg-background border border-border rounded-lg p-5 space-y-3"},pt={class:"grid grid-cols-3 gap-3"},gt={key:0,class:"text-xs text-red-400 flex items-center gap-1.5 pt-1"},xt={class:"bg-background border border-border rounded-lg p-5 space-y-3"},bt={class:"grid grid-cols-3 gap-3"},ht={key:0},ft={class:"flex items-baseline justify-between mb-3"},_t={class:"text-xs font-bold text-white uppercase tracking-wider"},wt={class:"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4"},yt={class:"flex items-start justify-between gap-2"},kt={class:"min-w-0"},St={class:"text-sm font-medium text-white truncate"},$t={class:"text-[10px] text-foreground-muted font-mono truncate"},Ft={class:"text-right shrink-0"},Mt={class:"text-xs font-mono text-white"},Bt={class:"grid grid-cols-3 gap-2"},Nt={class:"border-t border-border pt-3 grid grid-cols-2 gap-3 text-[11px]"},Ct={class:"font-mono text-white"},Lt={class:"font-mono text-white"},jt={class:"font-mono text-white",title:"Average memory used per invocation vs allocated limit"},At={class:"text-foreground-muted"},Rt={class:"font-mono text-white",title:"Average CPU cores consumed per invocation vs allocated"},Pt={key:0,class:"text-foreground-muted"},qt={key:1,class:"bg-background border border-border rounded-lg p-8 text-center space-y-4"},Et={__name:"Dashboard",setup(Tt){const p=E(),n=h(()=>p.metrics||{}),B=s=>p.poolHistory[s]||[],N=s=>s==null?y:`${s.toFixed(1)}%`,C=s=>s==null?"0":s.toFixed(1),f=s=>{const e=s||0;return e>=1024?`${(e/1024).toFixed(1)} GB`:`${Math.round(e)} MB`},_=s=>{const e=Number(s)||0;return e>=1e6?`${(e/1e6).toFixed(1)}M`:e>=1e3?`${(e/1e3).toFixed(1)}k`:String(e)},g=h(()=>n.value.host?.mem_total_mb??0),x=h(()=>n.value.host?.mem_reserved_mb??0),S=h(()=>Math.max(0,g.value-x.value)),L=h(()=>g.value>0?x.value/g.value*100:0);H(()=>p.connect()),I(()=>p.disconnect());const w={props:{label:String,value:[String,Number],icon:Object,hint:String},setup(s){return()=>l("div",{class:"bg-background border border-border rounded-lg p-5 flex flex-col h-full hover:border-primary/50 transition-colors group"},[l("div",{class:"flex items-center justify-between mb-3"},[l("span",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide"},s.label),s.icon?l(s.icon,{class:"w-4 h-4 text-foreground-muted group-hover:text-primary"}):null]),l("div",{class:"text-2xl font-mono text-foreground leading-none"},String(s.value)),s.hint?l("div",{class:"text-[11px] text-foreground-muted mt-auto pt-3 leading-snug"},s.hint):null])}},j={props:{p50:Number,p95:Number,p99:Number},setup(s){return()=>{const e=[{label:"p50",ms:s.p50,color:"bg-success/70"},{label:"p95",ms:s.p95,color:"bg-warning/70"},{label:"p99",ms:s.p99,color:"bg-danger/70"}],o=Math.max(s.p50||0,s.p95||0,s.p99||0,1);return l("div",{class:"space-y-2.5"},e.map(d=>{const b=d.ms==null?0:d.ms/o*100;return l("div",{class:"space-y-1"},[l("div",{class:"flex items-baseline justify-between text-[11px]"},[l("span",{class:"font-mono uppercase text-foreground-muted tracking-wider"},d.label),l("span",{class:"font-mono text-white"},d.ms==null?y:`${d.ms}ms`)]),l("div",{class:"h-1.5 bg-surface rounded overflow-hidden"},[l("div",{class:`h-full ${d.color} transition-[width] duration-500 ease-out`,style:{width:`${b.toFixed(1)}%`}})])])}))}}},v={props:{label:String,value:[String,Number],hint:String},setup(s){return()=>l("div",{class:"bg-surface border border-border rounded p-3 flex flex-col h-full"},[l("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted"},s.label),l("div",{class:"text-lg font-mono text-white mt-0.5"},String(s.value??0)),s.hint?l("div",{class:"text-[10px] text-foreground-muted mt-auto pt-1.5 leading-snug"},s.hint):null])}},k={props:{label:String,value:[String,Number],hint:String},setup(s){return()=>l("div",{class:"bg-surface border border-border rounded p-2.5 flex flex-col h-full"},[l("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted"},s.label),l("div",{class:"text-base font-mono text-white mt-0.5 leading-none"},String(s.value??0)),s.hint?l("div",{class:"text-[10px] text-foreground-muted mt-auto pt-1.5"},s.hint):null])}},A={props:{total:{type:Number,required:!0},segments:{type:Array,required:!0}},setup(s){return()=>{const e=s.total>0?s.total:1;return l("div",{class:"h-2.5 bg-surface rounded overflow-hidden flex",role:"img","aria-label":s.segments.map(o=>`${o.label}: ${o.value} of ${s.total}`).join("; ")},s.segments.map(o=>l("div",{class:`h-full ${o.color}`,style:{width:`${(o.value/e*100).toFixed(2)}%`},title:`${o.label}: ${o.value}`})))}}},R={props:{points:{type:Array,default:()=>[]}},setup(s){return()=>{const e=s.points||[];if(e.length<2)return l("div",{class:"h-8 flex items-center text-[10px] text-foreground-muted"},"(collecting samples…)");const o=Math.max(...e,1),d=100,b=32,P=d/(e.length-1),q=e.map((T,$)=>{const U=($*P).toFixed(2),D=(b-T/o*b).toFixed(2);return`${$===0?"M":"L"}${U},${D}`}).join(" ");return l("svg",{viewBox:`0 0 ${d} ${b}`,class:"w-full h-8 text-blue-400",preserveAspectRatio:"none"},[l("path",{d:q,fill:"none",stroke:"currentColor","stroke-width":"1.5"})])}}};return(s,e)=>(m(),c("div",X,[e[21]||(e[21]=t("div",null,[t("h1",{class:"text-xl font-semibold text-white tracking-tight"},"System Overview"),t("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"},"Live snapshot of what your platform is doing right now.")],-1)),t("div",Z,[r(w,{label:"Functions",hint:"Deployed in this workspace",value:u(p).functionsCount,icon:u(O)},null,8,["value","icon"]),r(w,{label:"In flight",hint:"Requests being handled right now",value:n.value.active_requests??0,icon:u(V)},null,8,["value","icon"]),r(w,{label:"Invocations",hint:"Total calls served since the platform started",value:_(n.value.totals?.invocations??0),icon:u(Q)},null,8,["value","icon"]),r(w,{label:"Cold starts",hint:"Calls that had to spawn a fresh sandbox",value:N(n.value.rates?.cold_start_pct),icon:u(K)},null,8,["value","icon"])]),t("div",tt,[t("div",et,[e[1]||(e[1]=t("div",{class:"mb-3"},[t("h2",{class:"text-xs font-bold text-white uppercase tracking-wider"}," Response time "),t("div",{class:"text-[11px] text-foreground-muted mt-1"}," How long calls take to come back. p99 is the worst-case 1-in-100. ")],-1)),r(j,{p50:n.value.latency_ms?.p50,p95:n.value.latency_ms?.p95,p99:n.value.latency_ms?.p99},null,8,["p50","p95","p99"])]),t("div",st,[e[7]||(e[7]=t("div",null,[t("h2",{class:"text-xs font-bold text-white uppercase tracking-wider"}," Host machine "),t("div",{class:"text-[11px] text-foreground-muted mt-1"}," The server Orva is running on, and how much of its RAM your warm sandboxes are holding. ")],-1)),t("div",ot,[t("div",null,[e[2]||(e[2]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted"},"CPU cores",-1)),t("div",lt,a(n.value.host?.num_cpu??"?"),1),e[3]||(e[3]=t("div",{class:"text-[11px] text-foreground-muted mt-0.5"}," available to functions on this host ",-1))]),t("div",null,[e[4]||(e[4]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted"},"Memory in use",-1)),t("div",nt,[i(a(f(x.value))+" ",1),t("span",at,"/ "+a(f(g.value)),1)]),t("div",rt,a(L.value.toFixed(1))+"% reserved by warm sandbox pools ",1)])]),t("div",dt,[r(A,{total:g.value,segments:[{label:"Reserved by warm pools",value:x.value,color:"bg-info/70"},{label:"Free",value:S.value,color:"bg-success/40"}]},null,8,["total","segments"]),t("div",it,[t("span",ut,[e[5]||(e[5]=t("span",{class:"w-2 h-2 rounded-full bg-info/70"},null,-1)),i(" "+a(f(x.value))+" held by warm sandboxes ready to serve ",1)]),t("span",ct,[e[6]||(e[6]=t("span",{class:"w-2 h-2 rounded-full bg-success/40"},null,-1)),i(" "+a(f(S.value))+" free for new pools or other workloads ",1)])])])])]),t("div",mt,[t("div",vt,[e[9]||(e[9]=t("div",null,[t("h2",{class:"text-xs font-bold text-white uppercase tracking-wider"}," Builds "),t("div",{class:"text-[11px] text-foreground-muted mt-1"}," Where deploys go: extracted, dependencies installed, then activated. ")],-1)),t("div",pt,[r(v,{label:"In queue",value:n.value.build_queue?.pending??0,hint:"waiting to start"},null,8,["value"]),r(v,{label:"Build workers",value:n.value.build_queue?.workers??0,hint:"parallel slots"},null,8,["value"]),r(v,{label:"Built so far",value:_(n.value.totals?.builds??0),hint:"lifetime total"},null,8,["value"])]),(n.value.totals?.build_errors??0)>0?(m(),c("div",gt,[e[8]||(e[8]=t("span",{class:"w-1.5 h-1.5 rounded-full bg-red-400"},null,-1)),i(" "+a(n.value.totals.build_errors)+" build"+a(n.value.totals.build_errors===1?" has":"s have")+" failed since start ",1)])):F("",!0)]),t("div",xt,[e[10]||(e[10]=t("div",null,[t("h2",{class:"text-xs font-bold text-white uppercase tracking-wider"}," Sandbox activity "),t("div",{class:"text-[11px] text-foreground-muted mt-1"}," Each invocation runs inside an isolated nsjail sandbox process. ")],-1)),t("div",bt,[r(v,{label:"Running now",value:n.value.sandbox?.active??0,hint:"serving a request"},null,8,["value"]),r(v,{label:"Reused",value:_(n.value.totals?.warm_hits??0),hint:"warm-pool hits"},null,8,["value"]),r(v,{label:"Spawned fresh",value:_(n.value.totals?.cold_starts??0),hint:"cold starts"},null,8,["value"])])])]),(n.value.pools||[]).length?(m(),c("div",ht,[t("div",ft,[t("div",null,[t("h2",_t," Warm pools ("+a(n.value.pools.length)+") ",1),e[11]||(e[11]=t("div",{class:"text-[11px] text-foreground-muted mt-1"}," One pool per active function. Sandboxes stay ready so the next call doesn't pay a cold start. ",-1))])]),t("div",wt,[(m(!0),c(z,null,W(n.value.pools,o=>(m(),c("div",{key:o.function_id,class:"bg-background border border-border rounded-lg p-4 space-y-3"},[t("div",yt,[t("div",kt,[t("div",St,a(o.function_name||o.function_id),1),t("div",$t,a(o.function_id),1)]),t("div",Ft,[e[13]||(e[13]=t("div",{class:"text-[10px] text-foreground-muted"},"Target / cap",-1)),t("div",Mt,[i(a(o.target)+" ",1),e[12]||(e[12]=t("span",{class:"text-foreground-muted"},"/",-1)),i(" "+a(o.dynamic_max),1)])])]),t("div",Bt,[r(k,{label:"Ready",value:o.idle,hint:"idle workers"},null,8,["value"]),r(k,{label:"Busy",value:o.busy,hint:"serving now"},null,8,["value"]),r(k,{label:"Calls / sec",value:C(o.rate_ewma),hint:"recent rate"},null,8,["value"])]),t("div",null,[r(R,{points:B(o.function_id)},null,8,["points"]),e[14]||(e[14]=t("div",{class:"text-[10px] text-foreground-muted mt-1"}," Recent calls per second (last 5 min) ",-1))]),t("div",Nt,[t("div",null,[e[15]||(e[15]=t("div",{class:"text-foreground-muted"},"Spawned · killed",-1)),t("div",Ct,a(o.spawned)+" · "+a(o.killed),1)]),t("div",null,[e[16]||(e[16]=t("div",{class:"text-foreground-muted"},"Avg latency",-1)),t("div",Lt,a(o.latency_ewma_ms?.toFixed?.(1)??0)+" ms",1)]),t("div",null,[e[17]||(e[17]=t("div",{class:"text-foreground-muted"},"Avg memory",-1)),t("div",jt,[i(a(o.mem_used_avg_mb>0?"~"+Math.round(o.mem_used_avg_mb):u(y))+" ",1),t("span",At,"/ "+a(o.mem_limit_mb)+" MB",1)])]),t("div",null,[e[18]||(e[18]=t("div",{class:"text-foreground-muted"},"Avg CPU",-1)),t("div",Rt,[i(a(o.cpu_frac_avg>0&&o.cpu_limit>0?(o.cpu_frac_avg*o.cpu_limit).toFixed(2):u(y))+" ",1),o.cpu_limit>0?(m(),c("span",Pt,"/ "+a(o.cpu_limit)+" CPU",1)):F("",!0)])])])]))),128))])])):(m(),c("div",qt,[e[20]||(e[20]=t("div",null,[t("div",{class:"text-sm text-white"},"No warm pools yet"),t("div",{class:"text-xs text-foreground-muted mt-1 max-w-prose mx-auto leading-body"}," Deploy your first function to see live worker pools, latency, and cold-start rate land in the tiles above. ")],-1)),t("div",null,[r(Y,{onClick:e[0]||(e[0]=o=>s.$router.push("/functions/new"))},{default:G(()=>[r(u(J),{class:"w-4 h-4"}),e[19]||(e[19]=i(" Deploy your first function ",-1))]),_:1})])]))]))}};export{Et as default}; diff --git a/backend/internal/server/ui_dist/assets/Deployments-4xbYmAdp.js b/backend/internal/server/ui_dist/assets/Deployments-4xbYmAdp.js deleted file mode 100644 index eb47652..0000000 --- a/backend/internal/server/ui_dist/assets/Deployments-4xbYmAdp.js +++ /dev/null @@ -1,11 +0,0 @@ -import{c as de,C as ce,L as me,D as ve,o as pe,M as fe,a as n,b as s,k as h,d as i,h as b,_ as A,f as c,t as r,g as u,F as ee,p as te,ab as xe,ac as P,q as O,U as he,r as v,H as be,a1 as ge,j as a,s as E,w as se,n as U,ad as ye,z as J,a9 as _e}from"./index-WXhXpu06.js";import{E as g}from"./format-CsU4_SPu.js";import{D as ke}from"./Drawer-DFsQitq5.js";import{_ as Y}from"./StatusBadge-B3a6roHm.js";import{d as we}from"./rollbackDiff-Cvt2Ss82.js";import{C as Ce,G as oe}from"./git-compare-CqfimOR1.js";import{R as $e}from"./refresh-cw-CEunRyai.js";import{R as ae}from"./rotate-ccw-Cx2X0X0k.js";const De=de("circle-check",[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]]),Se={class:"space-y-6"},Re={class:"flex items-center justify-between"},Ee={class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"},Le={class:"flex items-center gap-2"},Ne={key:0,class:"bg-background border border-border rounded-lg p-4 flex items-center gap-4"},Ve={class:"w-10 h-10 rounded-md bg-success/15 border border-success/30 flex items-center justify-center shrink-0"},Fe={class:"flex-1 min-w-0"},Te={class:"flex items-center gap-2 flex-wrap"},Be={class:"text-xs px-2 py-0.5 rounded bg-success/15 text-success border border-success/30 font-mono"},je={key:0,class:"text-xs px-2 py-0.5 rounded bg-amber-500/15 text-amber-400 border border-amber-500/30"},ze={class:"text-xs text-foreground-muted mt-1 font-mono truncate"},Ie={key:1,class:"bg-red-950/30 border border-red-900/40 rounded p-3 text-xs text-red-300"},Me={class:"bg-background border border-border rounded-lg overflow-x-auto"},qe={class:"sm:hidden divide-y divide-border"},Ae=["onClick"],Oe={class:"flex items-start justify-between gap-2"},Ue={class:"min-w-0 flex-1"},Ge={class:"flex items-center gap-2 flex-wrap"},He={key:0,class:"px-1.5 py-0.5 rounded text-[10px] bg-success/15 text-success border border-success/30"},Pe={class:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-foreground-muted"},Je={key:0,class:"font-mono"},Ye={key:1},Ke={key:0,class:"px-6 py-8 text-center text-sm text-foreground-muted"},Qe={class:"hidden sm:table w-full text-sm text-left"},We={class:"divide-y divide-border"},Xe=["onClick"],Ze={class:"px-6 py-4 font-mono text-xs"},et={class:"flex items-center gap-2"},tt={key:0,class:"px-1.5 py-0.5 rounded text-[10px] bg-success/15 text-success border border-success/30 normal-case"},st={class:"px-6 py-4 text-foreground"},ot={class:"px-6 py-4"},at={class:"px-6 py-4 text-foreground-muted text-xs hidden md:table-cell"},nt={class:"px-6 py-4 text-foreground-muted font-mono text-xs hidden sm:table-cell"},lt={class:"px-6 py-4 text-foreground-muted font-mono text-xs hidden xl:table-cell"},rt={class:"inline-flex items-center gap-2 justify-end"},it={key:2,class:"text-foreground-muted/50"},ut={key:3,class:"text-foreground-muted/30"},dt={key:0},ct={key:0,class:"p-6 text-sm text-foreground-muted"},mt={key:1,class:"p-5 space-y-4"},vt={class:"flex items-center gap-2 flex-wrap"},pt={key:0,class:"inline-flex items-center px-2.5 py-1 rounded text-xs border bg-background font-mono text-foreground-muted"},ft={class:"grid grid-cols-2 gap-3 text-sm"},xt={key:0},ht={class:"bg-red-950/30 border border-red-900/40 rounded p-3 text-xs text-red-300 font-mono whitespace-pre-wrap break-words"},bt={class:"flex items-center justify-between mb-2"},gt={key:0,class:"text-[10px] text-green-400"},yt={class:"bg-surface border border-border rounded p-3 text-xs text-foreground font-mono overflow-auto max-h-96 whitespace-pre-wrap break-words"},Lt={__name:"Deployments",setup(_t){const G=ce(),ne=ge(),x=O(()=>ne.params.name),L=v(null),d=v(null),y=v([]),C=v(!1),N=v(""),$=v(!1),V=t=>t&&t.status==="succeeded"&&t.code_hash&&!p(t),_=O(()=>y.value.find(e=>p(e))?.id||null),H=t=>t&&t.status==="succeeded"&&t.code_hash&&!p(t)&&_.value,K=async t=>{if(!L.value||!t?.id||$.value)return;const e=(t.code_hash||"").slice(0,12);let m=`Code hash ${e}. Current ${d.value?"v"+d.value.version:"version"} stays in history.`;try{const q=(await P(t.id))?.data?.snapshot;if(q&&d.value){const R=we(d.value,q);R.length?m=`Rolling back to v${t.version} (code ${e}) will also change: - -${R.join(` -`)} - -Secrets keep their current values — they aren't part of the rollback.`:m=`Rolling back to v${t.version} (code ${e}). Settings and env are already identical, so only the code changes.`}}catch{}if(_.value&&_.value!==t.id&&(m+=` - -Full source diff: ${window.location.origin}/web/functions/${x.value}/diff?from=${t.id}&to=${_.value}`),!!await G.ask({title:`Restore v${t.version}?`,message:m,confirmLabel:"Rollback"})){$.value=!0;try{await _e(L.value,{deployment_id:t.id}),await S()}catch(f){const q=f.response?.data?.error?.code||"",R=f.response?.data?.error?.message||f.message||"Rollback failed";q==="VERSION_GCD"?G.notify({title:"Version unavailable",message:`This version has been garbage-collected and can no longer be restored. - -${R}`,danger:!0}):G.notify({title:"Rollback failed",message:R,danger:!0})}finally{$.value=!1}}},p=t=>d.value&&t.version===d.value.version&&t.status==="succeeded",F=v(!1),l=v(null),T=v([]),B=v(!1);let j=null;const le=O(()=>l.value?`Deployment · ${l.value.id?.substring(0,14)}`:"Deployment"),re=O(()=>T.value.join(` -`)),D=t=>t?new Date(t).toLocaleString():g,z={props:{label:String,value:[String,Number],mono:Boolean},setup(t){return()=>J("div",{class:"bg-surface border border-border rounded p-3"},[J("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},t.label),J("div",{class:["text-sm text-white",t.mono&&"font-mono text-xs"].filter(Boolean)},String(t.value))])}},ie=async()=>{const e=((await be()).data.functions||[]).find(m=>m.name===x.value);if(!e)throw new Error(`Function "${x.value}" not found`);return e},S=async()=>{C.value=!0,N.value="";try{const t=await ie();L.value=t.id,d.value=t;const e=await xe(L.value,100);y.value=e.data.deployments||[]}catch(t){N.value=t.message||"Failed to load deployments"}finally{C.value=!1}},Q=async t=>{l.value=t,F.value=!0,T.value=[],B.value=!1;try{const e=await P(t.id);l.value={...t,...e.data}}catch{}try{const e=await ye(t.id,0,1e3);T.value=(e.data.logs||[]).map(W)}catch{}(t.status==="queued"||t.status==="building")&&ue(t.id)},W=t=>`[${t.stream||"log"}] ${t.line}`,ue=t=>{k();const e=new EventSource(`/api/v1/deployments/${t}/stream`);j=e,B.value=!0,e.addEventListener("log",m=>{try{const o=JSON.parse(m.data);T.value.push(W(o))}catch{}}),e.addEventListener("succeeded",()=>k(!0)),e.addEventListener("failed",()=>k(!0)),e.onerror=()=>{e.readyState===EventSource.CLOSED&&k()}},k=(t=!1)=>{if(j){try{j.close()}catch{}j=null}B.value=!1,t&&l.value&&(P(l.value.id).then(e=>{l.value={...l.value,...e.data}}).catch(()=>{}),S())};me(F,t=>{t||k()});const X=ve();let w=null;const Z=()=>{w||(w=setTimeout(()=>{w=null,S()},300))};let I=null,M=null;return pe(()=>{S(),I=X.subscribe("deployment",Z),M=X.subscribe("function",Z)}),fe(()=>{k(),I&&(I(),I=null),M&&(M(),M=null),w&&(clearTimeout(w),w=null)}),(t,e)=>{const m=he("router-link");return a(),n("div",Se,[s("div",Re,[s("div",null,[e[5]||(e[5]=s("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Deployments ",-1)),s("p",Ee,[e[4]||(e[4]=h(" History for ",-1)),i(m,{to:`/functions/${x.value}`,class:"text-white underline"},{default:b(()=>[h(r(x.value),1)]),_:1},8,["to"])])]),s("div",Le,[i(A,{variant:"secondary",onClick:e[0]||(e[0]=o=>t.$router.push(`/functions/${x.value}`))},{default:b(()=>[i(c(Ce),{class:"w-4 h-4 mr-2"}),e[6]||(e[6]=h(" Deploy New Version ",-1))]),_:1}),i(A,{variant:"secondary",onClick:S},{default:b(()=>[i(c($e),{class:E(["w-4 h-4 mr-2",{"animate-spin":C.value}])},null,8,["class"]),e[7]||(e[7]=h(" Refresh ",-1))]),_:1})])]),d.value?(a(),n("div",Ne,[s("div",Ve,[i(c(De),{class:"w-5 h-5 text-success"})]),s("div",Fe,[s("div",Te,[e[8]||(e[8]=s("span",{class:"text-sm text-white font-medium"},"Currently serving",-1)),s("span",Be," v"+r(d.value.version),1),d.value.status!=="active"?(a(),n("span",je," status: "+r(d.value.status),1)):u("",!0)]),s("div",ze," hash: "+r(d.value.code_hash||c(g))+" · runtime: "+r(d.value.runtime)+" · updated "+r(D(d.value.updated_at)),1)])])):u("",!0),N.value?(a(),n("div",Ie,r(N.value),1)):u("",!0),s("div",Me,[s("ul",qe,[(a(!0),n(ee,null,te(y.value,o=>(a(),n("li",{key:o.id,class:E(["px-4 py-3 cursor-pointer active:bg-surface-hover/50 transition-colors",p(o)?"bg-success/5":""]),onClick:f=>Q(o)},[s("div",Oe,[s("div",Ue,[s("div",Ge,[s("span",{class:E(["font-mono text-xs",p(o)?"text-white font-semibold":"text-foreground-muted"])},"v"+r(o.version),3),i(Y,{status:o.status},null,8,["status"]),p(o)?(a(),n("span",He,"Active")):u("",!0)]),s("div",Pe,[s("span",null,r(D(o.submitted_at)),1),o.duration_ms!=null?(a(),n("span",Je,r(o.duration_ms)+" ms",1)):u("",!0),o.phase?(a(),n("span",Ye,r(o.phase),1)):u("",!0)])]),s("div",{class:"shrink-0 flex items-center gap-1",onClick:e[1]||(e[1]=se(()=>{},["stop"]))},[H(o)?(a(),U(m,{key:0,to:{name:"function-diff",params:{name:x.value},query:{from:o.id,to:_.value}},class:"text-foreground-muted hover:text-white text-xs flex items-center gap-1 px-2 py-1",title:"Compare with active version"},{default:b(()=>[i(c(oe),{class:"w-3 h-3"}),e[9]||(e[9]=h(" Compare ",-1))]),_:1},8,["to"])):u("",!0),V(o)?(a(),U(A,{key:1,size:"xs",variant:"ghost",disabled:$.value,onClick:f=>K(o)},{default:b(()=>[i(c(ae),{class:"w-3 h-3"}),e[10]||(e[10]=h(" Rollback ",-1))]),_:1},8,["disabled","onClick"])):u("",!0)])])],10,Ae))),128)),!C.value&&y.value.length===0?(a(),n("li",Ke," No deployments yet for this function. ")):u("",!0)]),s("table",Qe,[e[14]||(e[14]=s("thead",{class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},[s("tr",null,[s("th",{class:"px-6 py-3 font-medium"}," Version "),s("th",{class:"px-6 py-3 font-medium"}," Submitted "),s("th",{class:"px-6 py-3 font-medium"}," Status "),s("th",{class:"px-6 py-3 font-medium hidden md:table-cell"}," Phase "),s("th",{class:"px-6 py-3 font-medium hidden sm:table-cell"}," Duration "),s("th",{class:"px-6 py-3 font-medium hidden xl:table-cell"}," Deployment ID "),s("th",{class:"px-6 py-3 font-medium text-right"}," Actions ")])],-1)),s("tbody",We,[(a(!0),n(ee,null,te(y.value,o=>(a(),n("tr",{key:o.id,class:E(["hover:bg-surface/50 transition-colors cursor-pointer",p(o)?"bg-success/5":""]),onClick:f=>Q(o)},[s("td",Ze,[s("div",et,[s("span",{class:E(["text-white",p(o)?"font-semibold":"text-foreground-muted"])},"v"+r(o.version),3),p(o)?(a(),n("span",tt,"Active")):u("",!0)])]),s("td",st,r(D(o.submitted_at)),1),s("td",ot,[i(Y,{status:o.status},null,8,["status"])]),s("td",at,r(o.phase||c(g)),1),s("td",nt,r(o.duration_ms!=null?o.duration_ms+"ms":c(g)),1),s("td",lt,r(o.id?.substring(0,14)),1),s("td",{class:"px-6 py-4 text-right text-xs",onClick:e[2]||(e[2]=se(()=>{},["stop"]))},[s("div",rt,[H(o)?(a(),U(m,{key:0,to:{name:"function-diff",params:{name:x.value},query:{from:o.id,to:_.value}},class:"text-foreground-muted hover:text-white flex items-center gap-1",title:"Compare with active version"},{default:b(()=>[i(c(oe),{class:"w-3 h-3"}),e[11]||(e[11]=h(" Compare ",-1))]),_:1},8,["to"])):u("",!0),V(o)?(a(),U(A,{key:1,size:"xs",variant:"ghost",disabled:$.value,onClick:f=>K(o)},{default:b(()=>[i(c(ae),{class:"w-3 h-3"}),e[12]||(e[12]=h(" Rollback ",-1))]),_:1},8,["disabled","onClick"])):u("",!0),!V(o)&&o.source==="rollback"?(a(),n("span",it,"via rollback")):!H(o)&&!V(o)?(a(),n("span",ut,r(c(g)),1)):u("",!0)])])],10,Xe))),128)),!C.value&&y.value.length===0?(a(),n("tr",dt,[...e[13]||(e[13]=[s("td",{colspan:"7",class:"px-6 py-8 text-center text-foreground-muted"}," No deployments yet for this function. ",-1)])])):u("",!0)])])]),i(ke,{modelValue:F.value,"onUpdate:modelValue":e[3]||(e[3]=o=>F.value=o),title:le.value,width:"640px"},{default:b(()=>[l.value?(a(),n("div",mt,[s("div",vt,[i(Y,{status:l.value.status},null,8,["status"]),l.value.phase?(a(),n("span",pt,r(l.value.phase),1)):u("",!0)]),s("div",ft,[i(z,{label:"Version",value:`v${l.value.version}`,mono:""},null,8,["value"]),i(z,{label:"Duration",value:l.value.duration_ms!=null?l.value.duration_ms+" ms":c(g)},null,8,["value"]),i(z,{label:"Submitted",value:D(l.value.submitted_at)},null,8,["value"]),i(z,{label:"Finished",value:l.value.finished_at?D(l.value.finished_at):c(g)},null,8,["value"])]),l.value.error_message?(a(),n("div",xt,[e[15]||(e[15]=s("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},"Error",-1)),s("pre",ht,r(l.value.error_message),1)])):u("",!0),s("div",null,[s("div",bt,[e[16]||(e[16]=s("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted"},"Build log",-1)),B.value?(a(),n("span",gt,"live")):u("",!0)]),s("pre",yt,r(re.value||"(no logs available)"),1)])])):(a(),n("div",ct,"Nothing selected."))]),_:1},8,["modelValue","title"])])}}};export{Lt as default}; diff --git a/backend/internal/server/ui_dist/assets/Deployments-Du8WdSzI.js b/backend/internal/server/ui_dist/assets/Deployments-Du8WdSzI.js new file mode 100644 index 0000000..b01b346 --- /dev/null +++ b/backend/internal/server/ui_dist/assets/Deployments-Du8WdSzI.js @@ -0,0 +1,11 @@ +import{G as de,I as ce,Y as me,o as pe,J as ve,a as n,b as s,k as h,d as i,h as b,_ as G,f as c,t as r,g as u,F as ee,p as te,aj as fe,ak as J,q as M,a5 as xe,r as p,a0 as he,ab as be,j as a,s as E,w as se,n as O,al as ge,z as P,ah as _e}from"./index-CmY58qNN.js";import{E as g}from"./format-CsU4_SPu.js";import{D as ye}from"./Drawer-B7DqDJXO.js";import{_ as Y}from"./StatusBadge-MvZdFvHS.js";import{d as ke}from"./rollbackDiff-Cvt2Ss82.js";import{C as we}from"./circle-check-Z_CRCWca.js";import{C as Ce,G as oe}from"./git-compare-BYn-27do.js";import{R as $e}from"./refresh-cw-63KivPtM.js";import{R as ae}from"./rotate-ccw-DQrtkmfg.js";import"./circle-DeGmlwna.js";import"./clock-CbnKorJ0.js";const Se={class:"space-y-6"},De={class:"flex items-center justify-between"},Re={class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"},Ee={class:"flex items-center gap-2"},Ne={key:0,class:"bg-background border border-border rounded-lg p-4 flex items-center gap-4"},Ve={class:"w-10 h-10 rounded-md bg-success/15 border border-success/30 flex items-center justify-center shrink-0"},Fe={class:"flex-1 min-w-0"},Le={class:"flex items-center gap-2 flex-wrap"},Te={class:"text-xs px-2 py-0.5 rounded bg-success/15 text-success border border-success/30 font-mono"},je={key:0,class:"text-xs px-2 py-0.5 rounded bg-amber-500/15 text-amber-400 border border-amber-500/30"},Be={class:"text-xs text-foreground-muted mt-1 font-mono truncate"},Ie={key:1,class:"bg-red-950/30 border border-red-900/40 rounded p-3 text-xs text-red-300"},qe={class:"bg-background border border-border rounded-lg overflow-x-auto"},ze={class:"sm:hidden divide-y divide-border"},Ae=["onClick"],Ge={class:"flex items-start justify-between gap-2"},Me={class:"min-w-0 flex-1"},Oe={class:"flex items-center gap-2 flex-wrap"},Ue={key:0,class:"px-1.5 py-0.5 rounded text-[10px] bg-success/15 text-success border border-success/30"},He={class:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-foreground-muted"},Je={key:0,class:"font-mono"},Pe={key:1},Ye={key:0,class:"px-6 py-8 text-center text-sm text-foreground-muted"},Ke={class:"hidden sm:table w-full text-sm text-left"},Qe={class:"divide-y divide-border"},We=["onClick"],Xe={class:"px-6 py-4 font-mono text-xs"},Ze={class:"flex items-center gap-2"},et={key:0,class:"px-1.5 py-0.5 rounded text-[10px] bg-success/15 text-success border border-success/30 normal-case"},tt={class:"px-6 py-4 text-foreground"},st={class:"px-6 py-4"},ot={class:"px-6 py-4 text-foreground-muted text-xs hidden md:table-cell"},at={class:"px-6 py-4 text-foreground-muted font-mono text-xs hidden sm:table-cell"},nt={class:"px-6 py-4 text-foreground-muted font-mono text-xs hidden xl:table-cell"},lt={class:"inline-flex items-center gap-2 justify-end"},rt={key:2,class:"text-foreground-muted/50"},it={key:3,class:"text-foreground-muted/30"},ut={key:0},dt={key:0,class:"p-6 text-sm text-foreground-muted"},ct={key:1,class:"p-5 space-y-4"},mt={class:"flex items-center gap-2 flex-wrap"},pt={key:0,class:"inline-flex items-center px-2.5 py-1 rounded text-xs border bg-background font-mono text-foreground-muted"},vt={class:"grid grid-cols-2 gap-3 text-sm"},ft={key:0},xt={class:"bg-red-950/30 border border-red-900/40 rounded p-3 text-xs text-red-300 font-mono whitespace-pre-wrap break-words"},ht={class:"flex items-center justify-between mb-2"},bt={key:0,class:"text-[10px] text-green-400"},gt={class:"bg-surface border border-border rounded p-3 text-xs text-foreground font-mono overflow-auto max-h-96 whitespace-pre-wrap break-words"},Ft={__name:"Deployments",setup(_t){const U=de(),ne=be(),x=M(()=>ne.params.name),N=p(null),d=p(null),_=p([]),C=p(!1),V=p(""),$=p(!1),F=t=>t&&t.status==="succeeded"&&t.code_hash&&!v(t),y=M(()=>_.value.find(e=>v(e))?.id||null),H=t=>t&&t.status==="succeeded"&&t.code_hash&&!v(t)&&y.value,K=async t=>{if(!N.value||!t?.id||$.value)return;const e=(t.code_hash||"").slice(0,12);let m=`Code hash ${e}. Current ${d.value?"v"+d.value.version:"version"} stays in history.`;try{const A=(await J(t.id))?.data?.snapshot;if(A&&d.value){const R=ke(d.value,A);R.length?m=`Rolling back to v${t.version} (code ${e}) will also change: + +${R.join(` +`)} + +Secrets keep their current values — they aren't part of the rollback.`:m=`Rolling back to v${t.version} (code ${e}). Settings and env are already identical, so only the code changes.`}}catch{}if(y.value&&y.value!==t.id&&(m+=` + +Full source diff: ${window.location.origin}/web/functions/${x.value}/diff?from=${t.id}&to=${y.value}`),!!await U.ask({title:`Restore v${t.version}?`,message:m,confirmLabel:"Rollback"})){$.value=!0;try{await _e(N.value,{deployment_id:t.id}),await D()}catch(f){const A=f.response?.data?.error?.code||"",R=f.response?.data?.error?.message||f.message||"Rollback failed";A==="VERSION_GCD"?U.notify({title:"Version unavailable",message:`This version has been garbage-collected and can no longer be restored. + +${R}`,danger:!0}):U.notify({title:"Rollback failed",message:R,danger:!0})}finally{$.value=!1}}},v=t=>d.value&&t.version===d.value.version&&t.status==="succeeded",L=p(!1),l=p(null),T=p([]),j=p(!1);let B=null;const le=M(()=>l.value?`Deployment · ${l.value.id?.substring(0,14)}`:"Deployment"),re=M(()=>T.value.join(` +`)),S=t=>t?new Date(t).toLocaleString():g,I={props:{label:String,value:[String,Number],mono:Boolean},setup(t){return()=>P("div",{class:"bg-surface border border-border rounded p-3"},[P("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},t.label),P("div",{class:["text-sm text-white",t.mono&&"font-mono text-xs"].filter(Boolean)},String(t.value))])}},ie=async()=>{const e=((await he()).data.functions||[]).find(m=>m.name===x.value);if(!e)throw new Error(`Function "${x.value}" not found`);return e},D=async()=>{C.value=!0,V.value="";try{const t=await ie();N.value=t.id,d.value=t;const e=await fe(N.value,100);_.value=e.data.deployments||[]}catch(t){V.value=t.message||"Failed to load deployments"}finally{C.value=!1}},Q=async t=>{l.value=t,L.value=!0,T.value=[],j.value=!1;try{const e=await J(t.id);l.value={...t,...e.data}}catch{}try{const e=await ge(t.id,0,1e3);T.value=(e.data.logs||[]).map(W)}catch{}(t.status==="queued"||t.status==="building")&&ue(t.id)},W=t=>`[${t.stream||"log"}] ${t.line}`,ue=t=>{k();const e=new EventSource(`/api/v1/deployments/${t}/stream`);B=e,j.value=!0,e.addEventListener("log",m=>{try{const o=JSON.parse(m.data);T.value.push(W(o))}catch{}}),e.addEventListener("succeeded",()=>k(!0)),e.addEventListener("failed",()=>k(!0)),e.onerror=()=>{e.readyState===EventSource.CLOSED&&k()}},k=(t=!1)=>{if(B){try{B.close()}catch{}B=null}j.value=!1,t&&l.value&&(J(l.value.id).then(e=>{l.value={...l.value,...e.data}}).catch(()=>{}),D())};ce(L,t=>{t||k()});const X=me();let w=null;const Z=()=>{w||(w=setTimeout(()=>{w=null,D()},300))};let q=null,z=null;return pe(()=>{D(),q=X.subscribe("deployment",Z),z=X.subscribe("function",Z)}),ve(()=>{k(),q&&(q(),q=null),z&&(z(),z=null),w&&(clearTimeout(w),w=null)}),(t,e)=>{const m=xe("router-link");return a(),n("div",Se,[s("div",De,[s("div",null,[e[5]||(e[5]=s("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Deployments ",-1)),s("p",Re,[e[4]||(e[4]=h(" History for ",-1)),i(m,{to:`/functions/${x.value}`,class:"text-white underline"},{default:b(()=>[h(r(x.value),1)]),_:1},8,["to"])])]),s("div",Ee,[i(G,{variant:"secondary",onClick:e[0]||(e[0]=o=>t.$router.push(`/functions/${x.value}`))},{default:b(()=>[i(c(Ce),{class:"w-4 h-4 mr-2"}),e[6]||(e[6]=h(" Deploy New Version ",-1))]),_:1}),i(G,{variant:"secondary",onClick:D},{default:b(()=>[i(c($e),{class:E(["w-4 h-4 mr-2",{"animate-spin":C.value}])},null,8,["class"]),e[7]||(e[7]=h(" Refresh ",-1))]),_:1})])]),d.value?(a(),n("div",Ne,[s("div",Ve,[i(c(we),{class:"w-5 h-5 text-success"})]),s("div",Fe,[s("div",Le,[e[8]||(e[8]=s("span",{class:"text-sm text-white font-medium"},"Currently serving",-1)),s("span",Te," v"+r(d.value.version),1),d.value.status!=="active"?(a(),n("span",je," status: "+r(d.value.status),1)):u("",!0)]),s("div",Be," hash: "+r(d.value.code_hash||c(g))+" · runtime: "+r(d.value.runtime)+" · updated "+r(S(d.value.updated_at)),1)])])):u("",!0),V.value?(a(),n("div",Ie,r(V.value),1)):u("",!0),s("div",qe,[s("ul",ze,[(a(!0),n(ee,null,te(_.value,o=>(a(),n("li",{key:o.id,class:E(["px-4 py-3 cursor-pointer active:bg-surface-hover/50 transition-colors",v(o)?"bg-success/5":""]),onClick:f=>Q(o)},[s("div",Ge,[s("div",Me,[s("div",Oe,[s("span",{class:E(["font-mono text-xs",v(o)?"text-white font-semibold":"text-foreground-muted"])},"v"+r(o.version),3),i(Y,{status:o.status},null,8,["status"]),v(o)?(a(),n("span",Ue,"Active")):u("",!0)]),s("div",He,[s("span",null,r(S(o.submitted_at)),1),o.duration_ms!=null?(a(),n("span",Je,r(o.duration_ms)+" ms",1)):u("",!0),o.phase?(a(),n("span",Pe,r(o.phase),1)):u("",!0)])]),s("div",{class:"shrink-0 flex items-center gap-1",onClick:e[1]||(e[1]=se(()=>{},["stop"]))},[H(o)?(a(),O(m,{key:0,to:{name:"function-diff",params:{name:x.value},query:{from:o.id,to:y.value}},class:"text-foreground-muted hover:text-white text-xs flex items-center gap-1 px-2 py-1",title:"Compare with active version"},{default:b(()=>[i(c(oe),{class:"w-3 h-3"}),e[9]||(e[9]=h(" Compare ",-1))]),_:1},8,["to"])):u("",!0),F(o)?(a(),O(G,{key:1,size:"xs",variant:"ghost",disabled:$.value,onClick:f=>K(o)},{default:b(()=>[i(c(ae),{class:"w-3 h-3"}),e[10]||(e[10]=h(" Rollback ",-1))]),_:1},8,["disabled","onClick"])):u("",!0)])])],10,Ae))),128)),!C.value&&_.value.length===0?(a(),n("li",Ye," No deployments yet for this function. ")):u("",!0)]),s("table",Ke,[e[14]||(e[14]=s("thead",{class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},[s("tr",null,[s("th",{class:"px-6 py-3 font-medium"}," Version "),s("th",{class:"px-6 py-3 font-medium"}," Submitted "),s("th",{class:"px-6 py-3 font-medium"}," Status "),s("th",{class:"px-6 py-3 font-medium hidden md:table-cell"}," Phase "),s("th",{class:"px-6 py-3 font-medium hidden sm:table-cell"}," Duration "),s("th",{class:"px-6 py-3 font-medium hidden xl:table-cell"}," Deployment ID "),s("th",{class:"px-6 py-3 font-medium text-right"}," Actions ")])],-1)),s("tbody",Qe,[(a(!0),n(ee,null,te(_.value,o=>(a(),n("tr",{key:o.id,class:E(["hover:bg-surface/50 transition-colors cursor-pointer",v(o)?"bg-success/5":""]),onClick:f=>Q(o)},[s("td",Xe,[s("div",Ze,[s("span",{class:E(["text-white",v(o)?"font-semibold":"text-foreground-muted"])},"v"+r(o.version),3),v(o)?(a(),n("span",et,"Active")):u("",!0)])]),s("td",tt,r(S(o.submitted_at)),1),s("td",st,[i(Y,{status:o.status},null,8,["status"])]),s("td",ot,r(o.phase||c(g)),1),s("td",at,r(o.duration_ms!=null?o.duration_ms+"ms":c(g)),1),s("td",nt,r(o.id?.substring(0,14)),1),s("td",{class:"px-6 py-4 text-right text-xs",onClick:e[2]||(e[2]=se(()=>{},["stop"]))},[s("div",lt,[H(o)?(a(),O(m,{key:0,to:{name:"function-diff",params:{name:x.value},query:{from:o.id,to:y.value}},class:"text-foreground-muted hover:text-white flex items-center gap-1",title:"Compare with active version"},{default:b(()=>[i(c(oe),{class:"w-3 h-3"}),e[11]||(e[11]=h(" Compare ",-1))]),_:1},8,["to"])):u("",!0),F(o)?(a(),O(G,{key:1,size:"xs",variant:"ghost",disabled:$.value,onClick:f=>K(o)},{default:b(()=>[i(c(ae),{class:"w-3 h-3"}),e[12]||(e[12]=h(" Rollback ",-1))]),_:1},8,["disabled","onClick"])):u("",!0),!F(o)&&o.source==="rollback"?(a(),n("span",rt,"via rollback")):!H(o)&&!F(o)?(a(),n("span",it,r(c(g)),1)):u("",!0)])])],10,We))),128)),!C.value&&_.value.length===0?(a(),n("tr",ut,[...e[13]||(e[13]=[s("td",{colspan:"7",class:"px-6 py-8 text-center text-foreground-muted"}," No deployments yet for this function. ",-1)])])):u("",!0)])])]),i(ye,{modelValue:L.value,"onUpdate:modelValue":e[3]||(e[3]=o=>L.value=o),title:le.value,width:"640px"},{default:b(()=>[l.value?(a(),n("div",ct,[s("div",mt,[i(Y,{status:l.value.status},null,8,["status"]),l.value.phase?(a(),n("span",pt,r(l.value.phase),1)):u("",!0)]),s("div",vt,[i(I,{label:"Version",value:`v${l.value.version}`,mono:""},null,8,["value"]),i(I,{label:"Duration",value:l.value.duration_ms!=null?l.value.duration_ms+" ms":c(g)},null,8,["value"]),i(I,{label:"Submitted",value:S(l.value.submitted_at)},null,8,["value"]),i(I,{label:"Finished",value:l.value.finished_at?S(l.value.finished_at):c(g)},null,8,["value"])]),l.value.error_message?(a(),n("div",ft,[e[15]||(e[15]=s("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},"Error",-1)),s("pre",xt,r(l.value.error_message),1)])):u("",!0),s("div",null,[s("div",ht,[e[16]||(e[16]=s("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted"},"Build log",-1)),j.value?(a(),n("span",bt,"live")):u("",!0)]),s("pre",gt,r(re.value||"(no logs available)"),1)])])):(a(),n("div",dt,"Nothing selected."))]),_:1},8,["modelValue","title"])])}}};export{Ft as default}; diff --git a/backend/internal/server/ui_dist/assets/Docs-BfWixbII.css b/backend/internal/server/ui_dist/assets/Docs-BfWixbII.css new file mode 100644 index 0000000..29989af --- /dev/null +++ b/backend/internal/server/ui_dist/assets/Docs-BfWixbII.css @@ -0,0 +1 @@ +.docs-hero{position:relative;border:1px solid var(--color-border);border-radius:.85rem;overflow:hidden;background:radial-gradient(ellipse 60% 80% at 0% 0%,#553f831f,#553f8300 60%),linear-gradient(180deg,rgba(255,255,255,.012) 0%,transparent 100%),var(--color-background)}.docs-hero-bg{position:absolute;inset:0;pointer-events:none;background-image:radial-gradient(rgba(255,255,255,.025) 1px,transparent 1px);background-size:14px 14px;background-position:0 0;mask-image:linear-gradient(180deg,black 0%,transparent 100%);-webkit-mask-image:linear-gradient(180deg,black 0%,transparent 100%)}.docs-hero-content{position:relative;padding:1.6rem 1.5rem 1.2rem;display:flex;flex-direction:column;gap:1rem}@media(min-width:640px){.docs-hero-content{padding:2.2rem 2.2rem 1.6rem;gap:1.4rem}}.docs-hero-eyebrow{display:inline-flex;align-items:center;gap:.55rem}.docs-hero-eyebrow-mark{display:inline-block;width:6px;height:6px;border-radius:999px;background:var(--color-primary);box-shadow:0 0 12px #553f8399}.docs-hero-eyebrow-label{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.2em;text-transform:uppercase;color:var(--color-foreground-muted)}.docs-hero-row{display:flex;flex-direction:column;gap:1rem;align-items:stretch}@media(min-width:768px){.docs-hero-row{flex-direction:row;align-items:flex-start;justify-content:space-between;gap:2rem}}.docs-hero-text{max-width:56ch}.docs-hero-title{margin:0;font-family:var(--font-sans);font-size:30px;font-weight:600;letter-spacing:-.02em;line-height:1.05;color:var(--color-foreground)}@media(min-width:640px){.docs-hero-title{font-size:38px}}.docs-hero-sub{margin:.6rem 0 0;font-family:var(--font-sans);font-size:13.5px;line-height:1.55;color:var(--color-foreground-muted)}.docs-hero-actions{display:flex;align-items:flex-start;flex-shrink:0}.docs-hero-copy-icon{display:inline-flex;align-items:center;justify-content:center;width:2.2rem;height:2.2rem;border-radius:.45rem;border:1px solid var(--color-border);background:var(--color-surface);color:var(--color-foreground-muted);cursor:pointer;transition:color .12s,border-color .12s,background-color .12s}.docs-hero-copy-icon:hover{color:var(--color-foreground);border-color:var(--color-foreground-muted);background:var(--color-surface-hover, rgba(255, 255, 255, .04))}.docs-hero-copy-icon:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.docs-hero-copy-icon.copied{color:var(--color-success-fg);border-color:#4caf5073;background:#4caf5014}.docs-hero-copy{display:inline-flex;align-items:center;gap:.45rem;padding:.55rem .9rem;border-radius:.5rem;border:1px solid var(--color-border);background:var(--color-surface);color:var(--color-foreground);font-family:var(--font-sans);font-size:12.5px;font-weight:500;cursor:pointer;transition:background-color .12s,border-color .12s,color .12s}.docs-hero-copy:hover{background:var(--color-surface-hover, rgba(255, 255, 255, .04));border-color:var(--color-foreground-muted)}.docs-hero-copy:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.docs-hero-copy.copied{border-color:#4caf5066;color:var(--color-success-fg);background:#4caf5014}.docs-hero-toc{display:flex;flex-wrap:wrap;align-items:center;gap:.4rem;padding-top:1rem;border-top:1px dashed rgba(255,255,255,.05)}.docs-hero-toc-label{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.18em;text-transform:uppercase;color:var(--color-foreground-muted);margin-right:.3rem}.docs-hero-toc-link{display:inline-flex;align-items:center;gap:.4rem;padding:.32rem .6rem;border-radius:.4rem;border:1px solid var(--color-border);background:var(--color-surface);color:var(--color-foreground-muted);font-family:var(--font-sans);font-size:11.5px;font-weight:500;text-decoration:none;transition:color .12s,border-color .12s,background-color .12s}.docs-hero-toc-link:hover{color:var(--color-foreground);border-color:var(--color-foreground-muted)}.docs-hero-toc-link.active{color:var(--color-foreground);border-color:var(--color-primary);background:#553f832e}.docs-hero-toc-num{font-family:var(--font-mono);font-size:10px;letter-spacing:.04em;color:var(--color-foreground-muted);opacity:.7}.docs-hero-toc-link.active .docs-hero-toc-num{color:#ffffffd9;opacity:1}.doc-diagram{margin:0 0 .75rem;padding:1.2rem 1.2rem 1.4rem;border:1px solid var(--color-border);border-radius:.7rem;background:radial-gradient(ellipse 50% 80% at 100% 0%,rgba(85,63,131,.06) 0%,transparent 70%),var(--color-background)}.doc-diagram-cap{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.16em;text-transform:uppercase;color:var(--color-foreground-muted);margin:0 0 1rem}.doc-pipeline{display:flex;align-items:stretch;justify-content:center;gap:.4rem;overflow-x:auto;padding-bottom:.4rem}.doc-pipeline-stage{display:flex;align-items:center;gap:.6rem;padding:.65rem .85rem;border:1px solid var(--color-border);border-radius:.55rem;background:var(--color-surface);min-width:0;flex-shrink:0}.doc-pipeline-glyph{display:inline-flex;align-items:center;justify-content:center;width:1.6rem;height:1.6rem;border-radius:.4rem;background:#553f832e;border:1px solid rgba(85,63,131,.45);color:var(--color-foreground);font-family:var(--font-mono);font-size:13px;line-height:1}.doc-pipeline-label{display:flex;flex-direction:column;gap:.05rem;min-width:0}.doc-pipeline-name{font-family:var(--font-sans);font-size:12.5px;font-weight:600;color:var(--color-foreground);white-space:nowrap}.doc-pipeline-sub{font-family:var(--font-mono);font-size:10px;color:var(--color-foreground-muted);white-space:nowrap}.doc-pipeline-arrow{flex-shrink:0;align-self:center;width:1.6rem;height:1px;background:linear-gradient(90deg,#ffffff0d,#ffffff2e);position:relative}.doc-pipeline-arrow:after{content:"";position:absolute;right:-2px;top:50%;width:.42rem;height:.42rem;border-top:1px solid rgba(255,255,255,.35);border-right:1px solid rgba(255,255,255,.35);transform:translateY(-50%) rotate(45deg)}.doc-trace{display:flex;flex-direction:column;gap:.55rem}.doc-trace-axis{display:flex;justify-content:space-between;font-family:var(--font-mono);font-size:10px;color:var(--color-foreground-muted);padding:0 0 .2rem;border-bottom:1px dashed rgba(255,255,255,.05)}.doc-trace-row{display:grid;grid-template-columns:11rem 1fr 3rem;align-items:center;gap:.7rem;font-family:var(--font-sans)}.doc-trace-row.is-child .doc-trace-label,.doc-trace-row.is-grand .doc-trace-label{position:relative}.doc-trace-row.is-child .doc-trace-label:before,.doc-trace-row.is-grand .doc-trace-label:before{content:"└";position:absolute;left:-.7rem;top:.05rem;color:var(--color-foreground-muted);font-family:var(--font-mono);opacity:.55}.doc-trace-row.is-child .doc-trace-label,.doc-trace-row.is-grand .doc-trace-label{padding-left:1rem}.doc-trace-label{display:flex;align-items:center;gap:.45rem;min-width:0}.doc-trace-fn{font-size:12.5px;color:var(--color-foreground);font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.doc-trace-trigger{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.06em;text-transform:lowercase;color:var(--color-foreground-muted);border:1px solid var(--color-border);border-radius:.25rem;padding:.05rem .35rem;background:var(--color-background)}.doc-trace-track{position:relative;height:10px;background:#ffffff06;border-radius:999px;overflow:hidden}.doc-trace-bar{position:absolute;top:1px;bottom:1px;background:linear-gradient(90deg,#553f83d9,#553f838c);border-radius:999px;box-shadow:0 0 12px #553f8340}.doc-trace-row.is-child .doc-trace-bar{background:linear-gradient(90deg,#60a5facc,#60a5fa80);box-shadow:0 0 10px #60a5fa40}.doc-trace-row.is-grand .doc-trace-bar{background:linear-gradient(90deg,#34d399cc,#34d39980);box-shadow:0 0 10px #34d39940}.doc-trace-dur{font-family:var(--font-mono);font-size:10.5px;color:var(--color-foreground);text-align:right}.doc-trace-legend{font-family:var(--font-sans);font-size:11.5px;color:var(--color-foreground-muted);padding-top:.4rem;border-top:1px dashed rgba(255,255,255,.05)}.doc-webhook{display:grid;grid-template-columns:1fr;gap:.7rem;align-items:stretch}@media(min-width:768px){.doc-webhook{grid-template-columns:1fr 1.4fr 1fr}}.doc-webhook-actor{display:flex;flex-direction:column;gap:.4rem;padding:.85rem;border:1px solid var(--color-border);border-radius:.55rem;background:var(--color-surface)}.doc-webhook-actor-head{font-family:var(--font-mono);font-size:11px;font-weight:600;color:var(--color-foreground);letter-spacing:.06em}.doc-webhook-actor-body{display:flex;flex-direction:column;gap:.3rem;font-family:var(--font-sans);font-size:11.5px;color:var(--color-foreground-muted)}.doc-webhook-wire{position:relative;display:flex;flex-direction:column;align-items:stretch;justify-content:center;padding:.85rem .7rem;border:1px dashed rgba(85,63,131,.45);border-radius:.55rem;background:radial-gradient(ellipse 80% 100% at 50% 50%,rgba(85,63,131,.08) 0%,transparent 70%)}.doc-webhook-wire-line{display:none}.doc-webhook-wire-payload{display:flex;flex-direction:column;gap:.45rem;font-family:var(--font-sans);font-size:11.5px}.doc-webhook-wire-method{display:inline-flex;align-items:center;align-self:flex-start;padding:.15rem .55rem;border-radius:.3rem;background:#553f8340;border:1px solid rgba(85,63,131,.55);color:var(--color-foreground);font-family:var(--font-mono);font-size:10.5px;font-weight:600;letter-spacing:.08em}.doc-webhook-wire-headers{display:flex;flex-wrap:wrap;gap:.3rem}.doc-webhook-wire-headers code{font-family:var(--font-mono);font-size:10.5px;padding:.1rem .4rem;border-radius:.25rem;border:1px solid var(--color-border);background:var(--color-background);color:var(--color-foreground)}.doc-webhook-wire-sig{font-family:var(--font-mono);font-size:10.5px;color:var(--color-foreground-muted);padding-top:.2rem;border-top:1px dashed rgba(255,255,255,.05)}.prompt-collapse{position:relative;max-height:9.5rem;overflow:hidden;border-radius:.6rem;transition:max-height .28s ease}.prompt-collapse.expanded{max-height:7000px}.prompt-collapse-fade{position:absolute;inset:auto 0 0;height:4.5rem;pointer-events:none;background:linear-gradient(180deg,transparent 0%,var(--color-background) 85%);border-radius:0 0 .6rem .6rem}.prompt-expand-btn{display:inline-flex;align-items:center;gap:.4rem;margin-top:.5rem;padding:.45rem .85rem;border:1px solid var(--color-border);border-radius:.45rem;background:var(--color-surface);color:var(--color-foreground-muted);font-family:var(--font-sans);font-size:12px;font-weight:500;cursor:pointer;transition:color .12s,border-color .12s,background-color .12s}.prompt-expand-btn:hover{color:var(--color-foreground);border-color:var(--color-foreground-muted)}.prompt-expand-btn:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.doc-section-head{display:flex;align-items:flex-start;gap:.85rem}.doc-section-num{display:inline-flex;flex-shrink:0;align-items:center;justify-content:center;width:1.85rem;height:1.85rem;border-radius:.5rem;background:linear-gradient(135deg,var(--color-primary) 0%,var(--color-primary-hover) 100%);color:var(--color-primary-foreground);font-family:var(--font-mono);font-size:11px;font-weight:700;letter-spacing:.04em;box-shadow:0 0 0 1px #ffffff0a inset,0 6px 18px -8px #553f8399}.doc-section-title{font-family:var(--font-sans);font-size:1.05rem;line-height:1.25;font-weight:600;letter-spacing:-.01em;color:var(--color-foreground);margin:0}.doc-lede{font-family:var(--font-sans);font-size:13px;line-height:1.6;color:var(--color-foreground-muted);max-width:64ch;margin:.35rem 0 0}.doc-chip{display:inline-block;font-family:var(--font-mono);font-size:11.5px;line-height:1.4;padding:.1rem .4rem;margin:0 .05rem;border-radius:.3rem;background:var(--color-surface);border:1px solid var(--color-border);color:var(--color-foreground);white-space:nowrap;vertical-align:baseline}.doc-chip.break-all{white-space:normal;word-break:break-all}.doc-microlabel{font-family:var(--font-sans);font-size:12.5px;font-weight:600;letter-spacing:-.005em;color:var(--color-foreground);padding-left:.55rem;border-left:2px solid rgba(85,63,131,.55);text-transform:none}.doc-card .doc-microlabel{border-left:none;padding-left:0;font-size:12px;font-weight:600;color:var(--color-foreground)}.doc-card{position:relative;padding:.85rem .95rem;background:linear-gradient(180deg,rgba(255,255,255,.015) 0%,transparent 100%),var(--color-background);border:1px solid var(--color-border);border-radius:.6rem;display:flex;flex-direction:column;gap:.5rem;transition:border-color .16s}.doc-card:hover{border-color:#553f8399}.doc-card-body{font-family:var(--font-sans);font-size:13px;line-height:1.55;color:var(--color-foreground);display:flex;flex-wrap:wrap;gap:.3rem .35rem;align-items:center}.doc-card-body p{flex-basis:100%;margin:0;font-size:12.5px;line-height:1.55}.doc-step-label{display:flex;align-items:center;gap:.55rem;font-family:var(--font-sans);font-size:12.5px;font-weight:600;letter-spacing:-.005em;color:var(--color-foreground)}.doc-step-num{display:inline-flex;align-items:center;justify-content:center;width:1.1rem;height:1.1rem;border-radius:999px;background:#553f832e;border:1px solid rgba(85,63,131,.6);color:var(--color-foreground);font-size:10px;font-weight:700;letter-spacing:0}.doc-table-wrap{background:var(--color-background);border:1px solid var(--color-border);border-radius:.6rem;overflow:hidden}.doc-table{width:100%;border-collapse:collapse;font-family:var(--font-sans)}.doc-table thead{background:var(--color-surface);border-bottom:1px solid var(--color-border)}.doc-table thead th{text-align:left;padding:.7rem 1rem;font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--color-foreground-muted)}.doc-table tbody tr{border-top:1px solid var(--color-border);transition:background-color .12s}.doc-table tbody tr:first-child{border-top:0}.doc-table tbody tr:hover{background:#ffffff04}.doc-table td{padding:.75rem 1rem;vertical-align:top}.doc-cell-key{display:flex;align-items:center;gap:.5rem;font-family:var(--font-sans);font-size:13px;font-weight:500;color:var(--color-foreground)}.doc-cell-key code{font-family:var(--font-mono);font-size:12px;color:var(--color-foreground);font-weight:500}.doc-cell-mono{font-family:var(--font-mono);font-size:12px;color:var(--color-foreground-muted)}.doc-cell-body{font-family:var(--font-sans);font-size:12.5px;line-height:1.55;color:var(--color-foreground)}.doc-token-bar{display:flex;align-items:center;justify-content:space-between;gap:.85rem;flex-wrap:wrap;padding:.65rem .85rem;background:linear-gradient(90deg,rgba(85,63,131,.1) 0%,rgba(85,63,131,.02) 70%,transparent 100%),var(--color-background);border:1px solid var(--color-border);border-radius:.6rem}.doc-token-btn{display:inline-flex;align-items:center;gap:.45rem;padding:.45rem .85rem;font-family:var(--font-sans);font-size:12.5px;font-weight:500;color:var(--color-foreground);background:var(--color-surface);border:1px solid var(--color-border);border-radius:.4rem;cursor:pointer;transition:background .12s,border-color .12s}.doc-token-btn:hover{background:var(--color-surface-hover);border-color:#553f8399}.doc-token-btn:disabled{opacity:.5;cursor:not-allowed}.doc-details{background:var(--color-background);border:1px solid var(--color-border);border-radius:.6rem;overflow:hidden}.doc-details-summary{list-style:none;display:flex;align-items:center;gap:.5rem;padding:.7rem .95rem;cursor:pointer;font-family:var(--font-sans);font-size:13px;font-weight:500;color:var(--color-foreground);-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:background .12s}.doc-details-summary::-webkit-details-marker{display:none}.doc-details-summary:hover{background:#ffffff05}.doc-details[open]>.doc-details-summary{border-bottom:1px solid var(--color-border)}.doc-details-body{padding:.85rem}.codeblock{background:var(--color-background);border:1px solid var(--color-border);border-radius:.6rem;overflow:hidden}.codeblock-bar{display:flex;align-items:center;justify-content:space-between;padding:.45rem .85rem;background:var(--color-surface);border-bottom:1px solid var(--color-border)}.codeblock-lang{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--color-foreground-muted)}.codeblock-copy{display:inline-flex;align-items:center;gap:.35rem;padding:.3rem .6rem;background:var(--color-background);border:1px solid var(--color-border);border-radius:.35rem;color:var(--color-foreground-muted);font-family:var(--font-sans);font-size:11.5px;cursor:pointer;transition:color .12s,border-color .12s,background .12s}.codeblock-copy:hover{color:var(--color-foreground);border-color:#553f8399;background:var(--color-surface-hover)}.codeblock-pre{margin:0;padding:.95rem 1.1rem;overflow-x:auto;font-family:var(--font-mono);font-size:12.5px;line-height:1.6;color:var(--color-foreground-muted);background:var(--color-background)}.codeblock-pre code{background:transparent!important;padding:0!important;font-family:inherit;font-size:inherit;line-height:inherit}.tabbed{background:var(--color-background);border:1px solid var(--color-border);border-radius:.6rem;overflow:hidden}.tabbed-tabs{display:flex;flex-wrap:wrap;gap:0;background:var(--color-surface);border-bottom:1px solid var(--color-border);padding:0 .35rem}.tabbed-tab{position:relative;background:transparent;border:0;padding:.6rem .95rem;font-family:var(--font-sans);font-size:12.5px;font-weight:500;color:var(--color-foreground-muted);cursor:pointer;transition:color .12s}.tabbed-tab:hover{color:var(--color-foreground)}.tabbed-tab.active{color:var(--color-foreground);font-weight:600}.tabbed-tab.active:after{content:"";position:absolute;left:.6rem;right:.6rem;bottom:-1px;height:2px;background:var(--color-primary);border-radius:2px 2px 0 0}.tabbed-note{padding:.65rem .95rem;border-bottom:1px solid var(--color-border);background:var(--color-surface);color:var(--color-foreground-muted);font-family:var(--font-sans);font-size:12px;line-height:1.55}.tabbed>.codeblock{border:0;border-radius:0}.callout{display:flex;flex-direction:column;gap:.4rem;padding:.85rem 1rem;background:linear-gradient(90deg,rgba(85,63,131,.08) 0%,rgba(85,63,131,.01) 60%,transparent 100%),var(--color-background);border:1px solid var(--color-border);border-radius:.6rem}.callout-head{display:flex;align-items:center;gap:.5rem;font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--color-foreground-muted)}.callout-icon{width:.95rem;height:.95rem}.callout-body{display:flex;flex-wrap:wrap;gap:.3rem .4rem;align-items:center;font-family:var(--font-sans);font-size:13px;line-height:1.55;color:var(--color-foreground)}.ai-prompt-actions{display:flex;align-items:center;flex-wrap:wrap;gap:.75rem}.ai-copy-btn{display:inline-flex;align-items:center;gap:.4rem;padding:.4rem .75rem;border-radius:6px;border:1px solid var(--color-border);background:var(--color-surface);color:var(--color-foreground-muted);font-size:12px;cursor:pointer;transition:color .15s ease,border-color .15s ease}.ai-copy-btn:hover{color:#fff;border-color:var(--color-foreground-muted)}.ai-copy-btn.copied{color:var(--color-success-fg);border-color:#4ade8066}.codeblock-pre .hljs{background:transparent!important;color:inherit;padding:0} diff --git a/backend/internal/server/ui_dist/assets/Docs-CZWT_FLb.js b/backend/internal/server/ui_dist/assets/Docs-CZWT_FLb.js deleted file mode 100644 index 9bfe9b3..0000000 --- a/backend/internal/server/ui_dist/assets/Docs-CZWT_FLb.js +++ /dev/null @@ -1,329 +0,0 @@ -import{E as Bt}from"./format-CsU4_SPu.js";import{c as Ht}from"./clipboard-CmSw2rR-.js";import{a as Ss,b as Ts}from"./aiPrompts-Dgb3jxRL.js";import{C as xs,o as $t,M as As,a as ue,b as t,s as Ge,n as qe,f,F as De,p as Be,d as w,l as ae,k as g,h as Oe,b8 as Cs,t as R,g as Os,r as _e,q as V,az as Re,z as a,U as Rs,J as Ns,j as q,a2 as jt,I as Is}from"./index-WXhXpu06.js";import{C as ft}from"./check-CuO1EVch.js";import{C as mt}from"./copy-HWkx-vox.js";import{G as vt}from"./globe-DI_Jf14z.js";import{C as Ut}from"./chevron-right-DYfLurIz.js";import{K as Ze}from"./key-round-BMBM5azh.js";import{C as Ms}from"./chevron-down-CtdSqW4P.js";import{V as Ps}from"./variable-wg-kDh52.js";import{L as Ls}from"./lock-CDky0HD2.js";function Ds(l){return l&&l.__esModule&&Object.prototype.hasOwnProperty.call(l,"default")?l.default:l}var yt,zt;function Bs(){if(zt)return yt;zt=1;function l(e){return e instanceof Map?e.clear=e.delete=e.set=function(){throw new Error("map is read-only")}:e instanceof Set&&(e.add=e.clear=e.delete=function(){throw new Error("set is read-only")}),Object.freeze(e),Object.getOwnPropertyNames(e).forEach(n=>{const i=e[n],y=typeof i;(y==="object"||y==="function")&&!Object.isFrozen(i)&&l(i)}),e}class O{constructor(n){n.data===void 0&&(n.data={}),this.data=n.data,this.isMatchIgnored=!1}ignoreMatch(){this.isMatchIgnored=!0}}function b(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function k(e,...n){const i=Object.create(null);for(const y in e)i[y]=e[y];return n.forEach(function(y){for(const M in y)i[M]=y[M]}),i}const X="",Y=e=>!!e.scope,ne=(e,{prefix:n})=>{if(e.startsWith("language:"))return e.replace("language:","language-");if(e.includes(".")){const i=e.split(".");return[`${n}${i.shift()}`,...i.map((y,M)=>`${y}${"_".repeat(M+1)}`)].join(" ")}return`${n}${e}`};class N{constructor(n,i){this.buffer="",this.classPrefix=i.classPrefix,n.walk(this)}addText(n){this.buffer+=b(n)}openNode(n){if(!Y(n))return;const i=ne(n.scope,{prefix:this.classPrefix});this.span(i)}closeNode(n){Y(n)&&(this.buffer+=X)}value(){return this.buffer}span(n){this.buffer+=``}}const j=(e={})=>{const n={children:[]};return Object.assign(n,e),n};class U{constructor(){this.rootNode=j(),this.stack=[this.rootNode]}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(n){this.top.children.push(n)}openNode(n){const i=j({scope:n});this.add(i),this.stack.push(i)}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(n){return this.constructor._walk(n,this.rootNode)}static _walk(n,i){return typeof i=="string"?n.addText(i):i.children&&(n.openNode(i),i.children.forEach(y=>this._walk(n,y)),n.closeNode(i)),n}static _collapse(n){typeof n!="string"&&n.children&&(n.children.every(i=>typeof i=="string")?n.children=[n.children.join("")]:n.children.forEach(i=>{U._collapse(i)}))}}class Q extends U{constructor(n){super(),this.options=n}addText(n){n!==""&&this.add(n)}startScope(n){this.openNode(n)}endScope(){this.closeNode()}__addSublanguage(n,i){const y=n.root;i&&(y.scope=`language:${i}`),this.add(y)}toHTML(){return new N(this,this.options).value()}finalize(){return this.closeAllNodes(),!0}}function z(e){return e?typeof e=="string"?e:e.source:null}function L(e){return B("(?=",e,")")}function ie(e){return B("(?:",e,")*")}function Z(e){return B("(?:",e,")?")}function B(...e){return e.map(i=>z(i)).join("")}function pe(e){const n=e[e.length-1];return typeof n=="object"&&n.constructor===Object?(e.splice(e.length-1,1),n):{}}function re(...e){return"("+(pe(e).capture?"":"?:")+e.map(y=>z(y)).join("|")+")"}function he(e){return new RegExp(e.toString()+"|").exec("").length-1}function ke(e,n){const i=e&&e.exec(n);return i&&i.index===0}const Se=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;function ce(e,{joinWith:n}){let i=0;return e.map(y=>{i+=1;const M=i;let P=z(y),d="";for(;P.length>0;){const c=Se.exec(P);if(!c){d+=P;break}d+=P.substring(0,c.index),P=P.substring(c.index+c[0].length),c[0][0]==="\\"&&c[1]?d+="\\"+String(Number(c[1])+M):(d+=c[0],c[0]==="("&&i++)}return d}).map(y=>`(${y})`).join(n)}const ye=/\b\B/,He="[a-zA-Z]\\w*",Te="[a-zA-Z_]\\w*",$e="\\b\\d+(\\.\\d+)?",je="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",Ue="\\b(0b[01]+)",Xe="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",We=(e={})=>{const n=/^#![ ]*\//;return e.binary&&(e.begin=B(n,/.*\b/,e.binary,/\b.*/)),k({scope:"meta",begin:n,end:/$/,relevance:0,"on:begin":(i,y)=>{i.index!==0&&y.ignoreMatch()}},e)},Ee={begin:"\\\\[\\s\\S]",relevance:0},Ve={scope:"string",begin:"'",end:"'",illegal:"\\n",contains:[Ee]},Ne={scope:"string",begin:'"',end:'"',illegal:"\\n",contains:[Ee]},Je={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},H=function(e,n,i={}){const y=k({scope:"comment",begin:e,end:n,contains:[]},i);y.contains.push({scope:"doctag",begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)",end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0});const M=re("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/);return y.contains.push({begin:B(/[ ]+/,"(",M,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),y},J=H("//","$"),ge=H("/\\*","\\*/"),xe=H("#","$"),te={scope:"number",begin:$e,relevance:0},be={scope:"number",begin:je,relevance:0},Ie={scope:"number",begin:Ue,relevance:0},at={scope:"regexp",begin:/\/(?=[^/\n]*\/)/,end:/\/[gimuy]*/,contains:[Ee,{begin:/\[/,end:/\]/,relevance:0,contains:[Ee]}]},ee={scope:"title",begin:He,relevance:0},it={scope:"title",begin:Te,relevance:0},rt={begin:"\\.\\s*"+Te,relevance:0};var ze=Object.freeze({__proto__:null,APOS_STRING_MODE:Ve,BACKSLASH_ESCAPE:Ee,BINARY_NUMBER_MODE:Ie,BINARY_NUMBER_RE:Ue,COMMENT:H,C_BLOCK_COMMENT_MODE:ge,C_LINE_COMMENT_MODE:J,C_NUMBER_MODE:be,C_NUMBER_RE:je,END_SAME_AS_BEGIN:function(e){return Object.assign(e,{"on:begin":(n,i)=>{i.data._beginMatch=n[1]},"on:end":(n,i)=>{i.data._beginMatch!==n[1]&&i.ignoreMatch()}})},HASH_COMMENT_MODE:xe,IDENT_RE:He,MATCH_NOTHING_RE:ye,METHOD_GUARD:rt,NUMBER_MODE:te,NUMBER_RE:$e,PHRASAL_WORDS_MODE:Je,QUOTE_STRING_MODE:Ne,REGEXP_MODE:at,RE_STARTERS_RE:Xe,SHEBANG:We,TITLE_MODE:ee,UNDERSCORE_IDENT_RE:Te,UNDERSCORE_TITLE_MODE:it});function ct(e,n){e.input[e.index-1]==="."&&n.ignoreMatch()}function K(e,n){e.className!==void 0&&(e.scope=e.className,delete e.className)}function oe(e,n){n&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",e.__beforeBegin=ct,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords,e.relevance===void 0&&(e.relevance=0))}function Me(e,n){Array.isArray(e.illegal)&&(e.illegal=re(...e.illegal))}function m(e,n){if(e.match){if(e.begin||e.end)throw new Error("begin & end are not supported with match");e.begin=e.match,delete e.match}}function s(e,n){e.relevance===void 0&&(e.relevance=1)}const x=(e,n)=>{if(!e.beforeMatch)return;if(e.starts)throw new Error("beforeMatch cannot be used with starts");const i=Object.assign({},e);Object.keys(e).forEach(y=>{delete e[y]}),e.keywords=i.keywords,e.begin=B(i.beforeMatch,L(i.begin)),e.starts={relevance:0,contains:[Object.assign(i,{endsParent:!0})]},e.relevance=0,delete i.beforeMatch},u=["of","and","for","in","not","or","if","then","parent","list","value"],I="keyword";function le(e,n,i=I){const y=Object.create(null);return typeof e=="string"?M(i,e.split(" ")):Array.isArray(e)?M(i,e):Object.keys(e).forEach(function(P){Object.assign(y,le(e[P],n,P))}),y;function M(P,d){n&&(d=d.map(c=>c.toLowerCase())),d.forEach(function(c){const v=c.split("|");y[v[0]]=[P,Vt(v[0],v[1])]})}}function Vt(e,n){return n?Number(n):Yt(e)?0:1}function Yt(e){return u.includes(e.toLowerCase())}const _t={},Pe=e=>{console.error(e)},kt=(e,...n)=>{console.log(`WARN: ${e}`,...n)},Ke=(e,n)=>{_t[`${e}/${n}`]||(console.log(`Deprecated as of ${e}. ${n}`),_t[`${e}/${n}`]=!0)},Qe=new Error;function St(e,n,{key:i}){let y=0;const M=e[i],P={},d={};for(let c=1;c<=n.length;c++)d[c+y]=M[c],P[c+y]=!0,y+=he(n[c-1]);e[i]=d,e[i]._emit=P,e[i]._multi=!0}function Zt(e){if(Array.isArray(e.begin)){if(e.skip||e.excludeBegin||e.returnBegin)throw Pe("skip, excludeBegin, returnBegin not compatible with beginScope: {}"),Qe;if(typeof e.beginScope!="object"||e.beginScope===null)throw Pe("beginScope must be object"),Qe;St(e,e.begin,{key:"beginScope"}),e.begin=ce(e.begin,{joinWith:""})}}function Jt(e){if(Array.isArray(e.end)){if(e.skip||e.excludeEnd||e.returnEnd)throw Pe("skip, excludeEnd, returnEnd not compatible with endScope: {}"),Qe;if(typeof e.endScope!="object"||e.endScope===null)throw Pe("endScope must be object"),Qe;St(e,e.end,{key:"endScope"}),e.end=ce(e.end,{joinWith:""})}}function Qt(e){e.scope&&typeof e.scope=="object"&&e.scope!==null&&(e.beginScope=e.scope,delete e.scope)}function es(e){Qt(e),typeof e.beginScope=="string"&&(e.beginScope={_wrap:e.beginScope}),typeof e.endScope=="string"&&(e.endScope={_wrap:e.endScope}),Zt(e),Jt(e)}function ts(e){function n(d,c){return new RegExp(z(d),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(c?"g":""))}class i{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}addRule(c,v){v.position=this.position++,this.matchIndexes[this.matchAt]=v,this.regexes.push([v,c]),this.matchAt+=he(c)+1}compile(){this.regexes.length===0&&(this.exec=()=>null);const c=this.regexes.map(v=>v[1]);this.matcherRe=n(ce(c,{joinWith:"|"}),!0),this.lastIndex=0}exec(c){this.matcherRe.lastIndex=this.lastIndex;const v=this.matcherRe.exec(c);if(!v)return null;const F=v.findIndex((Ye,dt)=>dt>0&&Ye!==void 0),D=this.matchIndexes[F];return v.splice(0,F),Object.assign(v,D)}}class y{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(c){if(this.multiRegexes[c])return this.multiRegexes[c];const v=new i;return this.rules.slice(c).forEach(([F,D])=>v.addRule(F,D)),v.compile(),this.multiRegexes[c]=v,v}resumingScanAtSamePosition(){return this.regexIndex!==0}considerAll(){this.regexIndex=0}addRule(c,v){this.rules.push([c,v]),v.type==="begin"&&this.count++}exec(c){const v=this.getMatcher(this.regexIndex);v.lastIndex=this.lastIndex;let F=v.exec(c);if(this.resumingScanAtSamePosition()&&!(F&&F.index===this.lastIndex)){const D=this.getMatcher(0);D.lastIndex=this.lastIndex+1,F=D.exec(c)}return F&&(this.regexIndex+=F.position+1,this.regexIndex===this.count&&this.considerAll()),F}}function M(d){const c=new y;return d.contains.forEach(v=>c.addRule(v.begin,{rule:v,type:"begin"})),d.terminatorEnd&&c.addRule(d.terminatorEnd,{type:"end"}),d.illegal&&c.addRule(d.illegal,{type:"illegal"}),c}function P(d,c){const v=d;if(d.isCompiled)return v;[K,m,es,x].forEach(D=>D(d,c)),e.compilerExtensions.forEach(D=>D(d,c)),d.__beforeBegin=null,[oe,Me,s].forEach(D=>D(d,c)),d.isCompiled=!0;let F=null;return typeof d.keywords=="object"&&d.keywords.$pattern&&(d.keywords=Object.assign({},d.keywords),F=d.keywords.$pattern,delete d.keywords.$pattern),F=F||/\w+/,d.keywords&&(d.keywords=le(d.keywords,e.case_insensitive)),v.keywordPatternRe=n(F,!0),c&&(d.begin||(d.begin=/\B|\b/),v.beginRe=n(v.begin),!d.end&&!d.endsWithParent&&(d.end=/\B|\b/),d.end&&(v.endRe=n(v.end)),v.terminatorEnd=z(v.end)||"",d.endsWithParent&&c.terminatorEnd&&(v.terminatorEnd+=(d.end?"|":"")+c.terminatorEnd)),d.illegal&&(v.illegalRe=n(d.illegal)),d.contains||(d.contains=[]),d.contains=[].concat(...d.contains.map(function(D){return ss(D==="self"?d:D)})),d.contains.forEach(function(D){P(D,v)}),d.starts&&P(d.starts,c),v.matcher=M(v),v}if(e.compilerExtensions||(e.compilerExtensions=[]),e.contains&&e.contains.includes("self"))throw new Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");return e.classNameAliases=k(e.classNameAliases||{}),P(e)}function Tt(e){return e?e.endsWithParent||Tt(e.starts):!1}function ss(e){return e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map(function(n){return k(e,{variants:null},n)})),e.cachedVariants?e.cachedVariants:Tt(e)?k(e,{starts:e.starts?k(e.starts):null}):Object.isFrozen(e)?k(e):e}var ns="11.11.1";class os extends Error{constructor(n,i){super(n),this.name="HTMLInjectionError",this.html=i}}const lt=b,xt=k,At=Symbol("nomatch"),as=7,Ct=function(e){const n=Object.create(null),i=Object.create(null),y=[];let M=!0;const P="Could not find the language '{}', did you forget to load/include a language module?",d={disableAutodetect:!0,name:"Plain text",contains:[]};let c={ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",cssSelector:"pre code",languages:null,__emitter:Q};function v(o){return c.noHighlightRe.test(o)}function F(o){let h=o.className+" ";h+=o.parentNode?o.parentNode.className:"";const S=c.languageDetectRe.exec(h);if(S){const A=Ae(S[1]);return A||(kt(P.replace("{}",S[1])),kt("Falling back to no-highlight mode for this block.",o)),A?S[1]:"no-highlight"}return h.split(/\s+/).find(A=>v(A)||Ae(A))}function D(o,h,S){let A="",$="";typeof h=="object"?(A=o,S=h.ignoreIllegals,$=h.language):(Ke("10.7.0","highlight(lang, code, ...args) has been deprecated."),Ke("10.7.0",`Please use highlight(code, options) instead. -https://github.com/highlightjs/highlight.js/issues/2277`),$=o,A=h),S===void 0&&(S=!0);const de={code:A,language:$};tt("before:highlight",de);const Ce=de.result?de.result:Ye(de.language,de.code,S);return Ce.code=de.code,tt("after:highlight",Ce),Ce}function Ye(o,h,S,A){const $=Object.create(null);function de(r,p){return r.keywords[p]}function Ce(){if(!E.keywords){G.addText(C);return}let r=0;E.keywordPatternRe.lastIndex=0;let p=E.keywordPatternRe.exec(C),_="";for(;p;){_+=C.substring(r,p.index);const T=me.case_insensitive?p[0].toLowerCase():p[0],W=de(E,T);if(W){const[we,_s]=W;if(G.addText(_),_="",$[T]=($[T]||0)+1,$[T]<=as&&(ot+=_s),we.startsWith("_"))_+=p[0];else{const ks=me.classNameAliases[we]||we;fe(p[0],ks)}}else _+=p[0];r=E.keywordPatternRe.lastIndex,p=E.keywordPatternRe.exec(C)}_+=C.substring(r),G.addText(_)}function st(){if(C==="")return;let r=null;if(typeof E.subLanguage=="string"){if(!n[E.subLanguage]){G.addText(C);return}r=Ye(E.subLanguage,C,!0,Dt[E.subLanguage]),Dt[E.subLanguage]=r._top}else r=ut(C,E.subLanguage.length?E.subLanguage:null);E.relevance>0&&(ot+=r.relevance),G.__addSublanguage(r._emitter,r.language)}function se(){E.subLanguage!=null?st():Ce(),C=""}function fe(r,p){r!==""&&(G.startScope(p),G.addText(r),G.endScope())}function It(r,p){let _=1;const T=p.length-1;for(;_<=T;){if(!r._emit[_]){_++;continue}const W=me.classNameAliases[r[_]]||r[_],we=p[_];W?fe(we,W):(C=we,Ce(),C=""),_++}}function Mt(r,p){return r.scope&&typeof r.scope=="string"&&G.openNode(me.classNameAliases[r.scope]||r.scope),r.beginScope&&(r.beginScope._wrap?(fe(C,me.classNameAliases[r.beginScope._wrap]||r.beginScope._wrap),C=""):r.beginScope._multi&&(It(r.beginScope,p),C="")),E=Object.create(r,{parent:{value:E}}),E}function Pt(r,p,_){let T=ke(r.endRe,_);if(T){if(r["on:end"]){const W=new O(r);r["on:end"](p,W),W.isMatchIgnored&&(T=!1)}if(T){for(;r.endsParent&&r.parent;)r=r.parent;return r}}if(r.endsWithParent)return Pt(r.parent,p,_)}function ms(r){return E.matcher.regexIndex===0?(C+=r[0],1):(bt=!0,0)}function vs(r){const p=r[0],_=r.rule,T=new O(_),W=[_.__beforeBegin,_["on:begin"]];for(const we of W)if(we&&(we(r,T),T.isMatchIgnored))return ms(p);return _.skip?C+=p:(_.excludeBegin&&(C+=p),se(),!_.returnBegin&&!_.excludeBegin&&(C=p)),Mt(_,r),_.returnBegin?0:p.length}function ys(r){const p=r[0],_=h.substring(r.index),T=Pt(E,r,_);if(!T)return At;const W=E;E.endScope&&E.endScope._wrap?(se(),fe(p,E.endScope._wrap)):E.endScope&&E.endScope._multi?(se(),It(E.endScope,r)):W.skip?C+=p:(W.returnEnd||W.excludeEnd||(C+=p),se(),W.excludeEnd&&(C=p));do E.scope&&G.closeNode(),!E.skip&&!E.subLanguage&&(ot+=E.relevance),E=E.parent;while(E!==T.parent);return T.starts&&Mt(T.starts,r),W.returnEnd?0:p.length}function Es(){const r=[];for(let p=E;p!==me;p=p.parent)p.scope&&r.unshift(p.scope);r.forEach(p=>G.openNode(p))}let nt={};function Lt(r,p){const _=p&&p[0];if(C+=r,_==null)return se(),0;if(nt.type==="begin"&&p.type==="end"&&nt.index===p.index&&_===""){if(C+=h.slice(p.index,p.index+1),!M){const T=new Error(`0 width match regex (${o})`);throw T.languageName=o,T.badRule=nt.rule,T}return 1}if(nt=p,p.type==="begin")return vs(p);if(p.type==="illegal"&&!S){const T=new Error('Illegal lexeme "'+_+'" for mode "'+(E.scope||"")+'"');throw T.mode=E,T}else if(p.type==="end"){const T=ys(p);if(T!==At)return T}if(p.type==="illegal"&&_==="")return C+=` -`,1;if(gt>1e5&>>p.index*3)throw new Error("potential infinite loop, way more iterations than matches");return C+=_,_.length}const me=Ae(o);if(!me)throw Pe(P.replace("{}",o)),new Error('Unknown language: "'+o+'"');const ws=ts(me);let ht="",E=A||ws;const Dt={},G=new c.__emitter(c);Es();let C="",ot=0,Le=0,gt=0,bt=!1;try{if(me.__emitTokens)me.__emitTokens(h,G);else{for(E.matcher.considerAll();;){gt++,bt?bt=!1:E.matcher.considerAll(),E.matcher.lastIndex=Le;const r=E.matcher.exec(h);if(!r)break;const p=h.substring(Le,r.index),_=Lt(p,r);Le=r.index+_}Lt(h.substring(Le))}return G.finalize(),ht=G.toHTML(),{language:o,value:ht,relevance:ot,illegal:!1,_emitter:G,_top:E}}catch(r){if(r.message&&r.message.includes("Illegal"))return{language:o,value:lt(h),illegal:!0,relevance:0,_illegalBy:{message:r.message,index:Le,context:h.slice(Le-100,Le+100),mode:r.mode,resultSoFar:ht},_emitter:G};if(M)return{language:o,value:lt(h),illegal:!1,relevance:0,errorRaised:r,_emitter:G,_top:E};throw r}}function dt(o){const h={value:lt(o),illegal:!1,relevance:0,_top:d,_emitter:new c.__emitter(c)};return h._emitter.addText(o),h}function ut(o,h){h=h||c.languages||Object.keys(n);const S=dt(o),A=h.filter(Ae).filter(Nt).map(se=>Ye(se,o,!1));A.unshift(S);const $=A.sort((se,fe)=>{if(se.relevance!==fe.relevance)return fe.relevance-se.relevance;if(se.language&&fe.language){if(Ae(se.language).supersetOf===fe.language)return 1;if(Ae(fe.language).supersetOf===se.language)return-1}return 0}),[de,Ce]=$,st=de;return st.secondBest=Ce,st}function is(o,h,S){const A=h&&i[h]||S;o.classList.add("hljs"),o.classList.add(`language-${A}`)}function pt(o){let h=null;const S=F(o);if(v(S))return;if(tt("before:highlightElement",{el:o,language:S}),o.dataset.highlighted){console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",o);return}if(o.children.length>0&&(c.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."),console.warn("https://github.com/highlightjs/highlight.js/wiki/security"),console.warn("The element with unescaped HTML:"),console.warn(o)),c.throwUnescapedHTML))throw new os("One of your code blocks includes unescaped HTML.",o.innerHTML);h=o;const A=h.textContent,$=S?D(A,{language:S,ignoreIllegals:!0}):ut(A);o.innerHTML=$.value,o.dataset.highlighted="yes",is(o,S,$.language),o.result={language:$.language,re:$.relevance,relevance:$.relevance},$.secondBest&&(o.secondBest={language:$.secondBest.language,relevance:$.secondBest.relevance}),tt("after:highlightElement",{el:o,result:$,text:A})}function rs(o){c=xt(c,o)}const cs=()=>{et(),Ke("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")};function ls(){et(),Ke("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.")}let Ot=!1;function et(){function o(){et()}if(document.readyState==="loading"){Ot||window.addEventListener("DOMContentLoaded",o,!1),Ot=!0;return}document.querySelectorAll(c.cssSelector).forEach(pt)}function ds(o,h){let S=null;try{S=h(e)}catch(A){if(Pe("Language definition for '{}' could not be registered.".replace("{}",o)),M)Pe(A);else throw A;S=d}S.name||(S.name=o),n[o]=S,S.rawDefinition=h.bind(null,e),S.aliases&&Rt(S.aliases,{languageName:o})}function us(o){delete n[o];for(const h of Object.keys(i))i[h]===o&&delete i[h]}function ps(){return Object.keys(n)}function Ae(o){return o=(o||"").toLowerCase(),n[o]||n[i[o]]}function Rt(o,{languageName:h}){typeof o=="string"&&(o=[o]),o.forEach(S=>{i[S.toLowerCase()]=h})}function Nt(o){const h=Ae(o);return h&&!h.disableAutodetect}function hs(o){o["before:highlightBlock"]&&!o["before:highlightElement"]&&(o["before:highlightElement"]=h=>{o["before:highlightBlock"](Object.assign({block:h.el},h))}),o["after:highlightBlock"]&&!o["after:highlightElement"]&&(o["after:highlightElement"]=h=>{o["after:highlightBlock"](Object.assign({block:h.el},h))})}function gs(o){hs(o),y.push(o)}function bs(o){const h=y.indexOf(o);h!==-1&&y.splice(h,1)}function tt(o,h){const S=o;y.forEach(function(A){A[S]&&A[S](h)})}function fs(o){return Ke("10.7.0","highlightBlock will be removed entirely in v12.0"),Ke("10.7.0","Please use highlightElement now."),pt(o)}Object.assign(e,{highlight:D,highlightAuto:ut,highlightAll:et,highlightElement:pt,highlightBlock:fs,configure:rs,initHighlighting:cs,initHighlightingOnLoad:ls,registerLanguage:ds,unregisterLanguage:us,listLanguages:ps,getLanguage:Ae,registerAliases:Rt,autoDetection:Nt,inherit:xt,addPlugin:gs,removePlugin:bs}),e.debugMode=function(){M=!1},e.safeMode=function(){M=!0},e.versionString=ns,e.regex={concat:B,lookahead:L,either:re,optional:Z,anyNumberOfTimes:ie};for(const o in ze)typeof ze[o]=="object"&&l(ze[o]);return Object.assign(e,ze),e},Fe=Ct({});return Fe.newInstance=()=>Ct({}),yt=Fe,Fe.HighlightJS=Fe,Fe.default=Fe,yt}var Hs=Bs();const ve=Ds(Hs);function $s(l){const O=l.regex,b=new RegExp("[\\p{XID_Start}_]\\p{XID_Continue}*","u"),k=["and","as","assert","async","await","break","case","class","continue","def","del","elif","else","except","finally","for","from","global","if","import","in","is","lambda","match","nonlocal|10","not","or","pass","raise","return","try","while","with","yield"],N={$pattern:/[A-Za-z]\w+|__\w+__/,keyword:k,built_in:["__import__","abs","all","any","ascii","bin","bool","breakpoint","bytearray","bytes","callable","chr","classmethod","compile","complex","delattr","dict","dir","divmod","enumerate","eval","exec","filter","float","format","frozenset","getattr","globals","hasattr","hash","help","hex","id","input","int","isinstance","issubclass","iter","len","list","locals","map","max","memoryview","min","next","object","oct","open","ord","pow","print","property","range","repr","reversed","round","set","setattr","slice","sorted","staticmethod","str","sum","super","tuple","type","vars","zip"],literal:["__debug__","Ellipsis","False","None","NotImplemented","True"],type:["Any","Callable","Coroutine","Dict","List","Literal","Generic","Optional","Sequence","Set","Tuple","Type","Union"]},j={className:"meta",begin:/^(>>>|\.\.\.) /},U={className:"subst",begin:/\{/,end:/\}/,keywords:N,illegal:/#/},Q={begin:/\{\{/,relevance:0},z={className:"string",contains:[l.BACKSLASH_ESCAPE],variants:[{begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?'''/,end:/'''/,contains:[l.BACKSLASH_ESCAPE,j],relevance:10},{begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?"""/,end:/"""/,contains:[l.BACKSLASH_ESCAPE,j],relevance:10},{begin:/([fF][rR]|[rR][fF]|[fF])'''/,end:/'''/,contains:[l.BACKSLASH_ESCAPE,j,Q,U]},{begin:/([fF][rR]|[rR][fF]|[fF])"""/,end:/"""/,contains:[l.BACKSLASH_ESCAPE,j,Q,U]},{begin:/([uU]|[rR])'/,end:/'/,relevance:10},{begin:/([uU]|[rR])"/,end:/"/,relevance:10},{begin:/([bB]|[bB][rR]|[rR][bB])'/,end:/'/},{begin:/([bB]|[bB][rR]|[rR][bB])"/,end:/"/},{begin:/([fF][rR]|[rR][fF]|[fF])'/,end:/'/,contains:[l.BACKSLASH_ESCAPE,Q,U]},{begin:/([fF][rR]|[rR][fF]|[fF])"/,end:/"/,contains:[l.BACKSLASH_ESCAPE,Q,U]},l.APOS_STRING_MODE,l.QUOTE_STRING_MODE]},L="[0-9](_?[0-9])*",ie=`(\\b(${L}))?\\.(${L})|\\b(${L})\\.`,Z=`\\b|${k.join("|")}`,B={className:"number",relevance:0,variants:[{begin:`(\\b(${L})|(${ie}))[eE][+-]?(${L})[jJ]?(?=${Z})`},{begin:`(${ie})[jJ]?`},{begin:`\\b([1-9](_?[0-9])*|0+(_?0)*)[lLjJ]?(?=${Z})`},{begin:`\\b0[bB](_?[01])+[lL]?(?=${Z})`},{begin:`\\b0[oO](_?[0-7])+[lL]?(?=${Z})`},{begin:`\\b0[xX](_?[0-9a-fA-F])+[lL]?(?=${Z})`},{begin:`\\b(${L})[jJ](?=${Z})`}]},pe={className:"comment",begin:O.lookahead(/# type:/),end:/$/,keywords:N,contains:[{begin:/# type:/},{begin:/#/,end:/\b\B/,endsWithParent:!0}]},re={className:"params",variants:[{className:"",begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:N,contains:["self",j,B,z,l.HASH_COMMENT_MODE]}]};return U.contains=[z,B,j],{name:"Python",aliases:["py","gyp","ipython"],unicodeRegex:!0,keywords:N,illegal:/(<\/|\?)|=>/,contains:[j,B,{scope:"variable.language",match:/\bself\b/},{beginKeywords:"if",relevance:0},{match:/\bor\b/,scope:"keyword"},z,pe,l.HASH_COMMENT_MODE,{match:[/\bdef/,/\s+/,b],scope:{1:"keyword",3:"title.function"},contains:[re]},{variants:[{match:[/\bclass/,/\s+/,b,/\s*/,/\(\s*/,b,/\s*\)/]},{match:[/\bclass/,/\s+/,b]}],scope:{1:"keyword",3:"title.class",6:"title.class.inherited"}},{className:"meta",begin:/^[\t ]*@/,end:/(?=#)|$/,contains:[B,re,z]}]}}const Kt="[A-Za-z$_][0-9A-Za-z$_]*",js=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends","using"],Us=["true","false","null","undefined","NaN","Infinity"],qt=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],Xt=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],Wt=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],zs=["arguments","this","super","console","window","document","localStorage","sessionStorage","module","global"],Ks=[].concat(Wt,qt,Xt);function Ft(l){const O=l.regex,b=(H,{after:J})=>{const ge="",end:""},Y=/<[A-Za-z0-9\\._:-]+\s*\/>/,ne={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(H,J)=>{const ge=H[0].length+H.index,xe=H.input[ge];if(xe==="<"||xe===","){J.ignoreMatch();return}xe===">"&&(b(H,{after:ge})||J.ignoreMatch());let te;const be=H.input.substring(ge);if(te=be.match(/^\s*=/)){J.ignoreMatch();return}if((te=be.match(/^\s+extends\s+/))&&te.index===0){J.ignoreMatch();return}}},N={$pattern:Kt,keyword:js,literal:Us,built_in:Ks,"variable.language":zs},j="[0-9](_?[0-9])*",U=`\\.(${j})`,Q="0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*",z={className:"number",variants:[{begin:`(\\b(${Q})((${U})|\\.)?|(${U}))[eE][+-]?(${j})\\b`},{begin:`\\b(${Q})\\b((${U})\\b|\\.)?|(${U})\\b`},{begin:"\\b(0|[1-9](_?[0-9])*)n\\b"},{begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*n?\\b"},{begin:"\\b0[oO][0-7](_?[0-7])*n?\\b"},{begin:"\\b0[0-7]+n?\\b"}],relevance:0},L={className:"subst",begin:"\\$\\{",end:"\\}",keywords:N,contains:[]},ie={begin:".?html`",end:"",starts:{end:"`",returnEnd:!1,contains:[l.BACKSLASH_ESCAPE,L],subLanguage:"xml"}},Z={begin:".?css`",end:"",starts:{end:"`",returnEnd:!1,contains:[l.BACKSLASH_ESCAPE,L],subLanguage:"css"}},B={begin:".?gql`",end:"",starts:{end:"`",returnEnd:!1,contains:[l.BACKSLASH_ESCAPE,L],subLanguage:"graphql"}},pe={className:"string",begin:"`",end:"`",contains:[l.BACKSLASH_ESCAPE,L]},he={className:"comment",variants:[l.COMMENT(/\/\*\*(?!\/)/,"\\*/",{relevance:0,contains:[{begin:"(?=@[A-Za-z]+)",relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+"},{className:"type",begin:"\\{",end:"\\}",excludeEnd:!0,excludeBegin:!0,relevance:0},{className:"variable",begin:k+"(?=\\s*(-)|$)",endsParent:!0,relevance:0},{begin:/(?=[^\n])\s/,relevance:0}]}]}),l.C_BLOCK_COMMENT_MODE,l.C_LINE_COMMENT_MODE]},ke=[l.APOS_STRING_MODE,l.QUOTE_STRING_MODE,ie,Z,B,pe,{match:/\$\d+/},z];L.contains=ke.concat({begin:/\{/,end:/\}/,keywords:N,contains:["self"].concat(ke)});const Se=[].concat(he,L.contains),ce=Se.concat([{begin:/(\s*)\(/,end:/\)/,keywords:N,contains:["self"].concat(Se)}]),ye={className:"params",begin:/(\s*)\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:N,contains:ce},He={variants:[{match:[/class/,/\s+/,k,/\s+/,/extends/,/\s+/,O.concat(k,"(",O.concat(/\./,k),")*")],scope:{1:"keyword",3:"title.class",5:"keyword",7:"title.class.inherited"}},{match:[/class/,/\s+/,k],scope:{1:"keyword",3:"title.class"}}]},Te={relevance:0,match:O.either(/\bJSON/,/\b[A-Z][a-z]+([A-Z][a-z]*|\d)*/,/\b[A-Z]{2,}([A-Z][a-z]+|\d)+([A-Z][a-z]*)*/,/\b[A-Z]{2,}[a-z]+([A-Z][a-z]+|\d)*([A-Z][a-z]*)*/),className:"title.class",keywords:{_:[...qt,...Xt]}},$e={label:"use_strict",className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},je={variants:[{match:[/function/,/\s+/,k,/(?=\s*\()/]},{match:[/function/,/\s*(?=\()/]}],className:{1:"keyword",3:"title.function"},label:"func.def",contains:[ye],illegal:/%/},Ue={relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/,className:"variable.constant"};function Xe(H){return O.concat("(?!",H.join("|"),")")}const We={match:O.concat(/\b/,Xe([...Wt,"super","import"].map(H=>`${H}\\s*\\(`)),k,O.lookahead(/\s*\(/)),className:"title.function",relevance:0},Ee={begin:O.concat(/\./,O.lookahead(O.concat(k,/(?![0-9A-Za-z$_(])/))),end:k,excludeBegin:!0,keywords:"prototype",className:"property",relevance:0},Ve={match:[/get|set/,/\s+/,k,/(?=\()/],className:{1:"keyword",3:"title.function"},contains:[{begin:/\(\)/},ye]},Ne="(\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)|"+l.UNDERSCORE_IDENT_RE+")\\s*=>",Je={match:[/const|var|let/,/\s+/,k,/\s*/,/=\s*/,/(async\s*)?/,O.lookahead(Ne)],keywords:"async",className:{1:"keyword",3:"title.function"},contains:[ye]};return{name:"JavaScript",aliases:["js","jsx","mjs","cjs"],keywords:N,exports:{PARAMS_CONTAINS:ce,CLASS_REFERENCE:Te},illegal:/#(?![$_A-z])/,contains:[l.SHEBANG({label:"shebang",binary:"node",relevance:5}),$e,l.APOS_STRING_MODE,l.QUOTE_STRING_MODE,ie,Z,B,pe,he,{match:/\$\d+/},z,Te,{scope:"attr",match:k+O.lookahead(":"),relevance:0},Je,{begin:"("+l.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",relevance:0,contains:[he,l.REGEXP_MODE,{className:"function",begin:Ne,returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:l.UNDERSCORE_IDENT_RE,relevance:0},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/(\s*)\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:N,contains:ce}]}]},{begin:/,/,relevance:0},{match:/\s+/,relevance:0},{variants:[{begin:X.begin,end:X.end},{match:Y},{begin:ne.begin,"on:begin":ne.isTrulyOpeningTag,end:ne.end}],subLanguage:"xml",contains:[{begin:ne.begin,end:ne.end,skip:!0,contains:["self"]}]}]},je,{beginKeywords:"while if switch catch for"},{begin:"\\b(?!function)"+l.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{",returnBegin:!0,label:"func.def",contains:[ye,l.inherit(l.TITLE_MODE,{begin:k,className:"title.function"})]},{match:/\.\.\./,relevance:0},Ee,{match:"\\$"+k,relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"},contains:[ye]},We,Ue,He,Ve,{match:/\$[(.]/}]}}function Fs(l){const O={className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/,relevance:1.01},b={match:/[{}[\],:]/,className:"punctuation",relevance:0},k=["true","false","null"],X={scope:"literal",beginKeywords:k.join(" ")};return{name:"JSON",aliases:["jsonc"],keywords:{literal:k},contains:[O,b,l.QUOTE_STRING_MODE,X,l.C_NUMBER_MODE,l.C_LINE_COMMENT_MODE,l.C_BLOCK_COMMENT_MODE],illegal:"\\S"}}function Et(l){const O=l.regex,b={},k={begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[b]}]};Object.assign(b,{className:"variable",variants:[{begin:O.concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},k]});const X={className:"subst",begin:/\$\(/,end:/\)/,contains:[l.BACKSLASH_ESCAPE]},Y=l.inherit(l.COMMENT(),{match:[/(^|\s)/,/#.*$/],scope:{2:"comment"}}),ne={begin:/<<-?\s*(?=\w+)/,starts:{contains:[l.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,className:"string"})]}},N={className:"string",begin:/"/,end:/"/,contains:[l.BACKSLASH_ESCAPE,b,X]};X.contains.push(N);const j={match:/\\"/},U={className:"string",begin:/'/,end:/'/},Q={match:/\\'/},z={begin:/\$?\(\(/,end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},l.NUMBER_MODE,b]},L=["fish","bash","zsh","sh","csh","ksh","tcsh","dash","scsh"],ie=l.SHEBANG({binary:`(${L.join("|")})`,relevance:10}),Z={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[l.inherit(l.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0},B=["if","then","else","elif","fi","time","for","while","until","in","do","done","case","esac","coproc","function","select"],pe=["true","false"],re={match:/(\/[a-z._-]+)+/},he=["break","cd","continue","eval","exec","exit","export","getopts","hash","pwd","readonly","return","shift","test","times","trap","umask","unset"],ke=["alias","bind","builtin","caller","command","declare","echo","enable","help","let","local","logout","mapfile","printf","read","readarray","source","sudo","type","typeset","ulimit","unalias"],Se=["autoload","bg","bindkey","bye","cap","chdir","clone","comparguments","compcall","compctl","compdescribe","compfiles","compgroups","compquote","comptags","comptry","compvalues","dirs","disable","disown","echotc","echoti","emulate","fc","fg","float","functions","getcap","getln","history","integer","jobs","kill","limit","log","noglob","popd","print","pushd","pushln","rehash","sched","setcap","setopt","stat","suspend","ttyctl","unfunction","unhash","unlimit","unsetopt","vared","wait","whence","where","which","zcompile","zformat","zftp","zle","zmodload","zparseopts","zprof","zpty","zregexparse","zsocket","zstyle","ztcp"],ce=["chcon","chgrp","chown","chmod","cp","dd","df","dir","dircolors","ln","ls","mkdir","mkfifo","mknod","mktemp","mv","realpath","rm","rmdir","shred","sync","touch","truncate","vdir","b2sum","base32","base64","cat","cksum","comm","csplit","cut","expand","fmt","fold","head","join","md5sum","nl","numfmt","od","paste","ptx","pr","sha1sum","sha224sum","sha256sum","sha384sum","sha512sum","shuf","sort","split","sum","tac","tail","tr","tsort","unexpand","uniq","wc","arch","basename","chroot","date","dirname","du","echo","env","expr","factor","groups","hostid","id","link","logname","nice","nohup","nproc","pathchk","pinky","printenv","printf","pwd","readlink","runcon","seq","sleep","stat","stdbuf","stty","tee","test","timeout","tty","uname","unlink","uptime","users","who","whoami","yes"];return{name:"Bash",aliases:["sh","zsh"],keywords:{$pattern:/\b[a-z][a-z0-9._-]+\b/,keyword:B,literal:pe,built_in:[...he,...ke,"set","shopt",...Se,...ce]},contains:[ie,l.SHEBANG(),Z,z,Y,ne,re,N,j,U,Q,b]}}function Gs(l){const O=l.regex,b="HTTP/([32]|1\\.[01])",k=/[A-Za-z][A-Za-z0-9-]*/,X={className:"attribute",begin:O.concat("^",k,"(?=\\:\\s)"),starts:{contains:[{className:"punctuation",begin:/: /,relevance:0,starts:{end:"$",relevance:0}}]}},Y=[X,{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0}}];return{name:"HTTP",aliases:["https"],illegal:/\S/,contains:[{begin:"^(?="+b+" \\d{3})",end:/$/,contains:[{className:"meta",begin:b},{className:"number",begin:"\\b\\d{3}\\b"}],starts:{end:/\b\B/,illegal:/\S/,contains:Y}},{begin:"(?=^[A-Z]+ (.*?) "+b+"$)",end:/$/,contains:[{className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{className:"meta",begin:b},{className:"keyword",begin:"[A-Z]+"}],starts:{end:/\b\B/,illegal:/\S/,contains:Y}},l.inherit(X,{relevance:0})]}}const qs={class:"space-y-12 pb-16"},Xs={class:"docs-hero"},Ws={class:"docs-hero-content"},Vs={class:"docs-hero-row"},Ys={class:"docs-hero-actions"},Zs=["title","aria-label"],Js={class:"docs-hero-toc","aria-label":"Jump to docs section"},Qs=["href"],en={class:"docs-hero-toc-num"},tn={id:"handler",class:"space-y-5 scroll-mt-6"},sn={class:"doc-table-wrap"},nn={class:"doc-table"},on={class:"doc-cell-key"},an={class:"doc-cell-mono"},rn={class:"doc-cell-mono hidden sm:table-cell"},cn={class:"doc-cell-mono hidden md:table-cell"},ln={id:"deploy",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},dn={class:"grid grid-cols-1 lg:grid-cols-2 gap-3"},un={class:"space-y-2"},pn={class:"space-y-2"},hn={class:"space-y-2"},gn={id:"config",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},bn={class:"doc-table-wrap"},fn={class:"doc-table"},mn={class:"doc-cell-key whitespace-nowrap"},vn={class:"doc-cell-mono hidden sm:table-cell whitespace-nowrap"},yn={class:"doc-cell-body"},En={class:"space-y-2"},wn={class:"doc-details group"},_n={class:"doc-details-summary"},kn={class:"doc-details-body"},Sn={id:"sdk",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},Tn={class:"space-y-2"},xn={class:"space-y-2"},An={class:"space-y-2"},Cn={id:"schedules",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},On={class:"doc-section-head"},Rn={class:"doc-lede"},Nn={id:"webhooks",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},In={class:"doc-section-head"},Mn={class:"doc-lede"},Pn={class:"doc-table-wrap"},Ln={class:"doc-table"},Dn={class:"doc-cell-key whitespace-nowrap"},Bn={class:"doc-cell-body"},Hn={class:"space-y-2"},$n={id:"mcp",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},jn={class:"grid grid-cols-1 md:grid-cols-3 gap-3"},Un={class:"doc-card"},zn={class:"doc-card-body"},Kn={class:"doc-chip break-all"},Fn={class:"doc-token-bar"},Gn={class:"flex items-center gap-2 min-w-0 flex-1"},qn={key:0,class:"text-sm text-foreground-muted truncate"},Xn={key:1,class:"text-sm text-success truncate"},Wn={class:"doc-chip"},Vn=["disabled"],Yn={class:"doc-details group"},Zn={class:"doc-details-summary"},Jn={class:"doc-details-body space-y-4"},Qn={id:"generate",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},eo={class:"ai-prompt-actions"},to={key:0,class:"prompt-collapse-fade","aria-hidden":"true"},so=["aria-expanded"],no={id:"tracing",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},oo={class:"doc-table-wrap"},ao={class:"doc-table"},io={class:"doc-cell-key whitespace-nowrap"},ro={class:"doc-cell-body"},co={id:"errors",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},lo={class:"doc-table-wrap"},uo={class:"doc-table"},po={class:"doc-cell-key whitespace-nowrap"},ho={class:"doc-cell-body"},go={id:"cli",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},bo={class:"doc-prose"},fo={class:"doc-table-wrap"},mo={class:"doc-table"},vo={class:"doc-cell-key whitespace-nowrap"},yo={class:"doc-cell-mono"},Eo={class:"doc-cell-body hidden md:table-cell"},wo={class:"space-y-2"},_o={class:"space-y-2"},ko={class:"space-y-2"},So={class:"space-y-2"},To={class:"space-y-2"},xo=`# Available inside every running function — refresh per-invocation: -ORVA_TRACE_ID=tr_3e39f6991c66f140577c6021da7dd13b # one per causal chain -ORVA_SPAN_ID=sp_4ceba57f6b1c982e # this execution - -# Python: os.environ["ORVA_TRACE_ID"] -# Node.js: process.env.ORVA_TRACE_ID -# Reading them is optional — the platform records the trace for you.`,Ao=`// Function A — calls B via the SDK. Trace context flows automatically. -const { invoke, jobs } = require('orva') - -module.exports.handler = async (event) => { - // F2F call — B becomes a child span under A. - const result = await invoke('send_email', { to: event.email }) - - // Job enqueue — when this job runs (now or in 6 hours), the resulting - // execution lands in the SAME trace as A. - await jobs.enqueue('audit_log', { action: 'sent', to: event.email }) - - return { statusCode: 200, body: 'ok' } -}`,Co=`# Send the W3C traceparent header — Orva will adopt it as the trace root. -curl -H "traceparent: 00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01" \\ - https://orva.example.com/fn/myfn/ - -# Response always echoes: -# X-Trace-Id: tr_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`,Oo=`{ - "error": { - "code": "VALIDATION", - "message": "name must be lowercase and dash-separated", - "request_id": "req_abc123" - } -}`,Ro=`# 1. Generate an API key in the dashboard (Keys page) or via the API -# 2. Tell the CLI where to find your Orva and which key to use -orva login \\ - --endpoint https://orva.example.com \\ - --api-key orva_xxx_your_key_here - -# Writes ~/.orva/config.yaml. Subsequent commands need no flags. -orva system health # smoke test`,No=`# Init a project in cwd (creates orva.yaml + handler stub) -orva init - -# Deploy from a directory. Auto-detects handler.ts when tsconfig.json -# is present; else uses the runtime default (handler.js / handler.py). -orva deploy ./my-fn \\ - --name resize-image \\ - --runtime node24 - -# Override the entrypoint explicitly: -orva deploy ./my-fn --name api --runtime python314 --entrypoint app.py`,Io=`# Invoke a function by name or fn_: -orva invoke resize-image --data '{"url":"https://example.com/cat.jpg"}' - -# Recent executions: -orva logs resize-image - -# Single execution, with stdout/stderr: -orva logs resize-image --exec-id exec_abc123 - -# Live tail — SSE stream, Ctrl-C to stop: -orva logs resize-image --tail`,Mo=`# List keys (optionally by prefix) -orva kv list resize-image -orva kv list resize-image --prefix user: - -# Read / write / delete -orva kv get resize-image cache:home -orva kv put resize-image cache:home '{"hits":42}' --ttl 3600 -orva kv delete resize-image cache:home`,Po=`# Secrets — encrypted at rest, injected as env vars at spawn: -orva secrets set resize-image S3_BUCKET my-bucket -orva secrets list resize-image -orva secrets delete resize-image S3_BUCKET - -# Cron — fire a function on a schedule: -orva cron create --fn daily-report --expr '0 9 * * *' --tz Asia/Kolkata -orva cron list -orva cron update --enabled false # pause -orva cron delete - -# Jobs — fire-and-forget background queue: -orva jobs enqueue --fn send-email --data '{"to":"a@b.c"}' -orva jobs list --status pending -orva jobs retry -orva jobs delete - -# Outbound webhooks (system events): -orva webhooks create --url https://hooks.slack.com/... --events deployment.failed,job.failed -orva webhooks test - -# Inbound webhook triggers (external POST → function): -orva webhooks inbound create --fn order-handler --signature stripe`,Lo=`orva system health # daemon up + DB ok -orva system metrics # JSON metrics snapshot -orva system db-stats # on-disk breakdown (orva.db, WAL, functions/) -orva system vacuum # rewrite SQLite to reclaim freelist pages - -orva activity # last 50 activity rows -orva activity --tail # live feed (Ctrl-C) -orva activity --source mcp --limit 200 # MCP-only, last 200`,Gt="",Wo={__name:"Docs",setup(l){const O=xs();ve.registerLanguage("python",$s),ve.registerLanguage("javascript",Ft),ve.registerLanguage("js",Ft),ve.registerLanguage("json",Fs),ve.registerLanguage("bash",Et),ve.registerLanguage("shell",Et),ve.registerLanguage("sh",Et),ve.registerLanguage("http",Gs);const b=V(()=>window.location.origin),k=[{id:"handler",num:"01",label:"Handler"},{id:"deploy",num:"02",label:"Deploy"},{id:"config",num:"03",label:"Config"},{id:"sdk",num:"04",label:"SDK"},{id:"schedules",num:"05",label:"Schedules"},{id:"webhooks",num:"06",label:"Webhooks"},{id:"mcp",num:"07",label:"MCP"},{id:"generate",num:"08",label:"AI prompt"},{id:"tracing",num:"09",label:"Tracing"},{id:"errors",num:"10",label:"Errors"},{id:"cli",num:"11",label:"CLI"}],X=_e("handler");let Y=null;$t(()=>{if(typeof IntersectionObserver>"u")return;const m=new Set;Y=new IntersectionObserver(s=>{for(const x of s)x.isIntersecting?m.add(x.target.id):m.delete(x.target.id);for(const x of k)if(m.has(x.id)){X.value=x.id;break}},{rootMargin:"-20% 0px -70% 0px",threshold:0});for(const s of k){const x=document.getElementById(s.id);x&&Y.observe(x)}}),As(()=>{Y&&Y.disconnect()});const ne=Ts(),N=_e(!1);let j=null;const U=async()=>{await Ss()&&(N.value=!0,clearTimeout(j),j=setTimeout(()=>{N.value=!1},1500))},Q=Re({setup(){return()=>a("svg",{viewBox:"0 0 256 255",width:"14",height:"14",xmlns:"http://www.w3.org/2000/svg"},[a("defs",null,[a("linearGradient",{id:"pyg1",x1:"0",y1:"0",x2:"1",y2:"1"},[a("stop",{offset:"0","stop-color":"#387EB8"}),a("stop",{offset:"1","stop-color":"#366994"})]),a("linearGradient",{id:"pyg2",x1:"0",y1:"0",x2:"1",y2:"1"},[a("stop",{offset:"0","stop-color":"#FFE052"}),a("stop",{offset:"1","stop-color":"#FFC331"})])]),a("path",{fill:"url(#pyg1)",d:"M126.9 12c-58.3 0-54.7 25.3-54.7 25.3l.1 26.2H128v8H50.5S12 67.2 12 126.1c0 58.9 33.6 56.8 33.6 56.8h19.4v-27.4s-1-33.6 33.1-33.6h55.9s32 .5 32-30.9V43.5S191.7 12 126.9 12zM95.7 29.9a10 10 0 0 1 0 20 10 10 0 0 1 0-20z"}),a("path",{fill:"url(#pyg2)",d:"M129.1 243c58.3 0 54.7-25.3 54.7-25.3l-.1-26.2H128v-8h77.5s38.5 4.4 38.5-54.5c0-58.9-33.6-56.8-33.6-56.8h-19.4v27.4s1 33.6-33.1 33.6H102s-32-.5-32 30.9v52S64.3 243 129.1 243zm30.4-17.9a10 10 0 0 1 0-20 10 10 0 0 1 0 20z"})])}}),z=Re({setup(){return()=>a("svg",{viewBox:"0 0 256 280",width:"14",height:"14",xmlns:"http://www.w3.org/2000/svg"},[a("path",{fill:"#3F873F",d:"M128 0 12 67v146l116 67 116-67V67L128 0zm0 24.6 95 54.8v121.2l-95 54.8-95-54.8V79.4l95-54.8z"}),a("path",{fill:"#3F873F",d:"M128 64c-3 0-5.7.7-8 2.3L73 92c-5 2.7-8 8-8 13.6V169c0 5.6 3 10.7 8 13.5l13 7.4c6.3 3.1 8.5 3.1 11.4 3.1 9.4 0 14.8-5.7 14.8-15.6V117c0-1-.7-1.7-1.7-1.7H103c-1 0-1.7.7-1.7 1.7v60.2c0 4.4-4.5 8.7-11.8 5.1l-13.7-7.9a1.6 1.6 0 0 1-.8-1.4v-63.4c0-.6.3-1 .8-1.4l46.8-26.9c.4-.3 1-.3 1.4 0L171 110c.5.4.8.8.8 1.4V174a1.7 1.7 0 0 1-.8 1.4l-46.8 27c-.4.2-1 .2-1.4 0l-12-7.2c-.4-.2-.8-.2-1.2 0-3.4 1.9-4 2.2-7.2 3.3-.8.3-2 .7.4 2.1l15.7 9.3c2.5 1.4 5.3 2.2 8.2 2.2 2.9 0 5.7-.8 8.2-2.2L181 184c5-2.8 8-7.9 8-13.5V107c0-5.6-3-10.7-8-13.5l-46.7-26.7a17 17 0 0 0-6.3-2.8z"})])}}),L=Re({name:"DeployPipelineDiagram",setup(){const m=[{glyph:"▣",label:"Tarball",sub:"POST /deploy"},{glyph:"⟜",label:"Extract",sub:"untar → scratch dir"},{glyph:"◍",label:"Install",sub:"npm / pip"},{glyph:"⟐",label:"Compile",sub:"tsc (TypeScript)"},{glyph:"◉",label:"Activate",sub:"rename → current"},{glyph:"✦",label:"Warm pool",sub:"pre-spawn N workers"}];return()=>a("figure",{class:"doc-diagram"},[a("figcaption",{class:"doc-diagram-cap"},"Deploy pipeline"),a("div",{class:"doc-pipeline"},m.flatMap((s,x)=>{const u=a("div",{key:`s${x}`,class:"doc-pipeline-stage"},[a("div",{class:"doc-pipeline-glyph"},s.glyph),a("div",{class:"doc-pipeline-label"},[a("span",{class:"doc-pipeline-name"},s.label),a("span",{class:"doc-pipeline-sub"},s.sub)])]),I=xu/220*100;return()=>a("figure",{class:"doc-diagram"},[a("figcaption",{class:"doc-diagram-cap"},"Causal trace, one HTTP request and three spans"),a("div",{class:"doc-trace"},[a("div",{class:"doc-trace-axis"},[a("span",null,"0 ms"),a("span",null,"220 ms")]),...s.map(u=>a("div",{key:u.fn,class:["doc-trace-row",`is-${u.klass}`]},[a("div",{class:"doc-trace-label"},[a("span",{class:"doc-trace-fn"},u.fn),a("span",{class:"doc-trace-trigger"},u.trigger)]),a("div",{class:"doc-trace-track"},[a("div",{class:"doc-trace-bar",style:{left:`${x(u.start)}%`,width:`${x(u.dur)}%`},title:`+${u.start}ms · ${u.dur}ms`})]),a("div",{class:"doc-trace-dur"},`${u.dur}ms`)])),a("div",{class:"doc-trace-legend"},[a("span",null,"Same "),a("code",{class:"doc-chip"},"trace_id"),a("span",null," across all spans · "),a("code",{class:"doc-chip"},"parent_span_id"),a("span",null," chains them into a tree.")])])])}}),Z=Re({name:"WebhookDeliveryDiagram",setup(){return()=>a("figure",{class:"doc-diagram"},[a("figcaption",{class:"doc-diagram-cap"},"Signed webhook delivery"),a("div",{class:"doc-webhook"},[a("div",{class:"doc-webhook-actor"},[a("div",{class:"doc-webhook-actor-head"},"orvad"),a("div",{class:"doc-webhook-actor-body"},[a("span",null,"event fires"),a("code",{class:"doc-chip"},"deployment.succeeded")])]),a("div",{class:"doc-webhook-wire"},[a("div",{class:"doc-webhook-wire-line","aria-hidden":"true"}),a("div",{class:"doc-webhook-wire-payload"},[a("div",{class:"doc-webhook-wire-method"},"POST"),a("div",{class:"doc-webhook-wire-headers"},[a("code",null,"X-Orva-Event"),a("code",null,"X-Orva-Timestamp"),a("code",null,"X-Orva-Signature")]),a("div",{class:"doc-webhook-wire-sig"},"sha256=hex(hmac(secret, ts.body))")])]),a("div",{class:"doc-webhook-actor"},[a("div",{class:"doc-webhook-actor-head"},"your receiver"),a("div",{class:"doc-webhook-actor-body"},[a("span",null,"verify HMAC"),a("span",null,"→ 2xx within 15s or get retried")])])])])}}),B=V(()=>[{label:"Python",lang:"python",code:`def handler(event): - body = event.get("body") or {} - return { - "statusCode": 200, - "headers": {"Content-Type": "application/json"}, - "body": {"hello": body.get("name", "world")}, - }`},{label:"Node.js",lang:"js",code:`exports.handler = async (event) => { - const body = event.body || {}; - return { - statusCode: 200, - headers: { 'Content-Type': 'application/json' }, - body: { hello: body.name || 'world' }, - }; -};`}]),pe=V(()=>[{label:"curl",lang:"bash",code:`curl -X POST ${b.value}/fn/ \\ - -H 'Content-Type: application/json' \\ - -d '{"name": "Orva"}'`},{label:"fetch",lang:"js",code:`const res = await fetch('${b.value}/fn/', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: 'Orva' }), -}); -console.log(await res.json());`},{label:"Python",lang:"python",code:`import httpx - -r = httpx.post( - "${b.value}/fn/", - json={"name": "Orva"}, -) -print(r.json())`}]),re=[{id:"python314",name:"Python 3.14",entry:"handler.py",deps:"requirements.txt",icon:Q},{id:"python313",name:"Python 3.13",entry:"handler.py",deps:"requirements.txt",icon:Q},{id:"node24",name:"Node.js 24",entry:"handler.js",deps:"package.json",icon:z},{id:"node22",name:"Node.js 22",entry:"handler.js",deps:"package.json",icon:z}],he=[{field:"env_vars",purpose:"Plain config",body:"Plaintext config stored on the function record. Use for feature flags and non-secret settings.",icon:Ps,iconClass:"text-violet-300"},{field:"/secrets",purpose:"Encrypted",body:"AES-256-GCM at rest. Values decrypt only into the worker environment at spawn time.",icon:Ze,iconClass:"text-emerald-300"},{field:"network_mode",purpose:"Egress control",body:"none = isolated loopback. egress = outbound HTTPS allowed; firewall blocklist applies.",icon:vt,iconClass:"text-sky-300"},{field:"auth_mode",purpose:"Invoke gate",body:"none = public. platform_key = require Orva API key. signed = require HMAC.",icon:Ls,iconClass:"text-violet-300"},{field:"rate_limit_per_min",purpose:"Per-IP throttle",body:"Optional cap for public or webhook-facing functions. Exceeding it returns 429.",icon:Is,iconClass:"text-amber-300"}],ke=V(()=>`curl -X POST ${b.value}/api/v1/functions \\ - -H 'X-Orva-API-Key: ' \\ - -H 'Content-Type: application/json' \\ - -d '{"name":"hello","runtime":"python314","memory_mb":128,"cpus":0.5}'`),Se=V(()=>`tar czf code.tar.gz handler.py requirements.txt -curl -X POST ${b.value}/api/v1/functions//deploy \\ - -H 'X-Orva-API-Key: ' \\ - -F code=@code.tar.gz`),ce=V(()=>`curl -X POST ${b.value}/api/v1/functions//secrets \\ - -H 'X-Orva-API-Key: ' \\ - -H 'Content-Type: application/json' \\ - -d '{"key":"DATABASE_URL","value":"postgres://..."}'`),ye=V(()=>`# generate signature -SECRET='your-shared-secret-stored-in-function-secrets' -TS=$(date +%s) -BODY='{"hello":"world"}' -SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}') - -curl -X POST ${b.value}/fn/ \\ - -H "X-Orva-Timestamp: $TS" \\ - -H "X-Orva-Signature: sha256=$SIG" \\ - -H 'Content-Type: application/json' \\ - -d "$BODY"`),He=V(()=>[{label:"curl",lang:"bash",note:"Create a daily-9am schedule for an existing function. payload is delivered as the invoke body.",code:`curl -X POST ${b.value}/api/v1/functions//cron \\ - -H 'X-Orva-API-Key: ' \\ - -H 'Content-Type: application/json' \\ - -d '{ - "cron_expr": "0 9 * * *", - "enabled": true, - "payload": {"task": "daily-summary"} - }'`},{label:"Toggle / edit",lang:"bash",note:"PUT accepts any subset of {cron_expr, enabled, payload}; omitted fields keep their previous value. next_run_at is recomputed on expr changes.",code:`# pause -curl -X PUT ${b.value}/api/v1/functions//cron/ \\ - -H 'X-Orva-API-Key: ' \\ - -H 'Content-Type: application/json' \\ - -d '{"enabled": false}' - -# change schedule -curl -X PUT ${b.value}/api/v1/functions//cron/ \\ - -H 'X-Orva-API-Key: ' \\ - -H 'Content-Type: application/json' \\ - -d '{"cron_expr": "*/15 * * * *"}'`},{label:"List & delete",lang:"bash",note:"GET /api/v1/cron lists every schedule across functions (with function_name JOIN); per-function uses the nested route.",code:`# all schedules -curl ${b.value}/api/v1/cron \\ - -H 'X-Orva-API-Key: ' - -# delete one -curl -X DELETE ${b.value}/api/v1/functions//cron/ \\ - -H 'X-Orva-API-Key: '`}]),Te=[{label:"Python",lang:"python",code:`from orva import kv - -def handler(event): - # Store with optional TTL (seconds). 0 = no expiry. - kv.put("user:42", {"name": "Ada", "tier": "pro"}, ttl_seconds=3600) - - # Read; default returned if missing or expired. - user = kv.get("user:42", default=None) - - # List by prefix. - pages = kv.list(prefix="page:", limit=50) - - # Delete is idempotent. - kv.delete("user:42") - - return {"statusCode": 200, "body": str(user)}`},{label:"Node.js",lang:"js",code:`const { kv } = require('orva') - -exports.handler = async (event) => { - await kv.put('user:42', { name: 'Ada', tier: 'pro' }, { ttlSeconds: 3600 }) - - const user = await kv.get('user:42', null) - - const pages = await kv.list({ prefix: 'page:', limit: 50 }) - - await kv.delete('user:42') - - return { statusCode: 200, body: JSON.stringify(user) } -}`}],$e=[{label:"Python",lang:"python",code:`from orva import invoke, OrvaError - -def handler(event): - try: - # invoke() returns the downstream {statusCode, headers, body}. - # body is JSON-decoded when possible. - result = invoke("resize-image", {"url": event["body"]["url"]}) - return {"statusCode": 200, "body": result["body"]} - except OrvaError as e: - # 404 = function not found, 507 = call depth exceeded. - return {"statusCode": e.status or 502, "body": str(e)}`},{label:"Node.js",lang:"js",code:`const { invoke, OrvaError } = require('orva') - -exports.handler = async (event) => { - try { - const result = await invoke('resize-image', { url: event.body.url }) - return { statusCode: 200, body: result.body } - } catch (e) { - if (e instanceof OrvaError) { - return { statusCode: e.status || 502, body: e.message } - } - throw e - } -}`}],je=[{label:"Python",lang:"python",code:`from orva import jobs - -def handler(event): - # Fire-and-forget. Returns the job id immediately; the function - # body runs later via the scheduler. max_attempts retries with - # exponential backoff on 5xx / exception. - job_id = jobs.enqueue( - "send-welcome-email", - {"to": event["body"]["email"]}, - max_attempts=3, - ) - return {"statusCode": 202, "body": job_id}`},{label:"Node.js",lang:"js",code:`const { jobs } = require('orva') - -exports.handler = async (event) => { - const jobId = await jobs.enqueue( - 'send-welcome-email', - { to: event.body.email }, - { maxAttempts: 3 } - ) - return { statusCode: 202, body: jobId } -}`}],Ue=[{name:"deployment.succeeded",when:"A function build finished and the new version is active."},{name:"deployment.failed",when:"A build failed or was rejected."},{name:"function.created",when:"A new function row was created via POST /api/v1/functions."},{name:"function.updated",when:"A function config was edited via PUT /api/v1/functions/{id} (status flips during a deploy do NOT fire this; see deployment.*)."},{name:"function.deleted",when:"A function was removed."},{name:"execution.error",when:"An invocation finished with status=error or 5xx."},{name:"cron.failed",when:"A scheduled run failed (bad expr, missing fn, dispatch error, or 5xx)."},{name:"job.succeeded",when:"A queued background job finished successfully."},{name:"job.failed",when:"A queued job exhausted its retries (terminal failure)."}],Xe=[{label:"Python",lang:"python",note:"Run on the receiver. Reject anything that fails verification. The signature ensures the request really came from this Orva instance.",code:`import hmac, hashlib, time - -def verify(secret: str, ts: str, body: bytes, sig_header: str) -> bool: - if abs(time.time() - int(ts)) > 300: # 5-min skew window - return False - mac = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256) - expected = "sha256=" + mac.hexdigest() - return hmac.compare_digest(expected, sig_header) - -# In your Flask/FastAPI/etc. handler: -ts = request.headers["X-Orva-Timestamp"] -sig = request.headers["X-Orva-Signature"] -if not verify(WEBHOOK_SECRET, ts, request.get_data(), sig): - return "bad signature", 401`},{label:"Node.js",lang:"js",note:"Same shape as Stripe. Use timingSafeEqual to avoid sig-leak via timing.",code:`const crypto = require('crypto') - -function verify(secret, ts, body, sigHeader) { - if (Math.abs(Date.now() / 1000 - parseInt(ts, 10)) > 300) return false - const mac = crypto.createHmac('sha256', secret) - mac.update(ts + '.') - mac.update(body) - const expected = 'sha256=' + mac.digest('hex') - if (expected.length !== sigHeader.length) return false - return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sigHeader)) -} - -// In an express handler with raw body middleware: -app.post('/webhooks/orva', (req, res) => { - const ok = verify( - process.env.WEBHOOK_SECRET, - req.headers['x-orva-timestamp'], - req.body, // raw bytes — NOT parsed JSON - req.headers['x-orva-signature'] - ) - if (!ok) return res.status(401).send('bad signature') - res.sendStatus(200) -})`}],We=[{name:"http",desc:"Public HTTP request hit /fn//. Almost always a root span."},{name:"f2f",desc:"Another function called this one via orva.invoke(). Has a parent_span_id."},{name:"job",desc:"Background job runner picked up an enqueued job. Parent_span_id is whoever enqueued it."},{name:"cron",desc:"Scheduler fired a cron entry. Always a root span."},{name:"inbound",desc:"External webhook hit /webhook/{id}. Always a root span."},{name:"replay",desc:"Operator clicked Replay on a captured execution. Fresh trace, no link to original."},{name:"mcp",desc:"AI agent invoked the function via MCP invoke_function. Fresh trace."}],Ee=[{code:"VALIDATION",when:"Bad request body or path parameter."},{code:"UNAUTHORIZED",when:"Missing or invalid API key / session cookie."},{code:"NOT_FOUND",when:"Function, deployment, or secret doesn't exist."},{code:"RATE_LIMITED",when:"Too many requests; check the Retry-After header."},{code:"VERSION_GCD",when:"Rollback target was garbage-collected."},{code:"INSUFFICIENT_DISK",when:"Host is below min_free_disk_mb."}],Ve=[{cmd:"login",subs:Bt,purpose:"Save endpoint + API key to ~/.orva/config.yaml"},{cmd:"init",subs:Bt,purpose:"Scaffold an orva.yaml in the current directory"},{cmd:"deploy",subs:"[path]",purpose:"Package a directory and deploy as a function"},{cmd:"invoke",subs:"[name|id]",purpose:"POST to /fn// and print the response"},{cmd:"logs",subs:"[name|id] [--tail]",purpose:"List recent executions; --tail follows live via SSE"},{cmd:"functions",subs:"list / get / create / delete",purpose:"CRUD for the function registry"},{cmd:"cron",subs:"list / create / update / delete",purpose:"Manage cron schedules attached to functions"},{cmd:"jobs",subs:"list / enqueue / retry / delete",purpose:"Background queue management"},{cmd:"kv",subs:"list / get / put / delete",purpose:"Browse a function’s key/value store"},{cmd:"secrets",subs:"list / set / delete",purpose:"AES-256-GCM secrets per function"},{cmd:"webhooks",subs:"list / create / test / delete / inbound",purpose:"System-event subscribers + inbound triggers"},{cmd:"routes",subs:"list / set / delete",purpose:"Custom URL → function path mappings"},{cmd:"keys",subs:"list / create / revoke",purpose:"Manage API keys"},{cmd:"activity",subs:"[--tail] [--source web|api|...]",purpose:"Paginated activity rows; live SSE with --tail"},{cmd:"system",subs:"health / metrics / db-stats / vacuum",purpose:"Server diagnostics"},{cmd:"setup",subs:"[--skip-nsjail] [--skip-rootfs]",purpose:"Install nsjail + rootfs on a bare host"},{cmd:"serve",subs:"[--port N]",purpose:"Run as the server daemon (not the CLI client)"},{cmd:"completion",subs:"bash / zsh / fish / powershell",purpose:"Emit shell completion script"}],Ne=_e("");$t(async()=>{try{const m=await fetch("/web/docs.md",{cache:"no-cache"});m.ok&&(Ne.value=await m.text())}catch{}});const H=V(()=>Ne.value.replaceAll("{{ORIGIN}}",window.location.origin)),J=_e(!1);let ge=null;const xe=async()=>{await Ht(H.value)&&(J.value=!0,clearTimeout(ge),ge=setTimeout(()=>{J.value=!1},1500))},te=_e(!1),be=_e(""),Ie=_e(!1),at=V(()=>be.value.slice(0,12)),ee=V(()=>be.value||Gt),it=async()=>{if(!Ie.value){Ie.value=!0;try{const m=new Date().toISOString().slice(0,16).replace("T"," "),s=await Ns.post("/keys",{name:"MCP: "+m,permissions:["invoke","read","write","admin"]});be.value=s.data.key}catch(m){console.error("mint mcp key failed",m),O.notify({title:"Could not mint key",message:m?.response?.data?.error?.message||m.message||"Unknown error",danger:!0})}finally{Ie.value=!1}}},rt=V(()=>[{label:"Claude Code",lang:"bash",note:"Anthropic's `claude` CLI. Restart Claude Code afterwards; `/mcp` lists Orva's 70 tools.",code:`claude mcp add --transport http --scope user orva ${b.value}/mcp --header "Authorization: Bearer ${ee.value}"`},{label:"curl",lang:"bash",note:"Talk to MCP directly. Step 1 returns a session id (Mcp-Session-Id) that Step 2 references.",code:`curl -sD - -X POST ${b.value}/mcp \\ - -H 'Authorization: Bearer ${ee.value}' \\ - -H 'Content-Type: application/json' \\ - -H 'Accept: application/json, text/event-stream' \\ - -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}' - -curl -sX POST ${b.value}/mcp \\ - -H 'Authorization: Bearer ${ee.value}' \\ - -H 'Content-Type: application/json' \\ - -H 'Accept: application/json, text/event-stream' \\ - -H 'Mcp-Session-Id: ' \\ - -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'`}]),wt=V(()=>[{label:"Claude Desktop",lang:"json",note:"Paste into ~/Library/Application Support/Claude/claude_desktop_config.json (macOS), %APPDATA%\\Claude\\claude_desktop_config.json (Windows), or ~/.config/Claude/claude_desktop_config.json (Linux). Restart Claude Desktop.",code:`{ - "mcpServers": { - "orva": { - "url": "${b.value}/mcp", - "headers": { - "Authorization": "Bearer ${ee.value}" - } - } - } -}`},{label:"Cursor",lang:"bash",note:"Open the link in your browser. Cursor pops an approval dialog and writes ~/.cursor/mcp.json.",code:`cursor://anysphere.cursor-deeplink/mcp/install?name=orva&config=${ze.value}`},{label:"VS Code",lang:"bash",note:'User-scoped install via the Copilot-MCP `code --add-mcp` flag. Pick "Workspace" at the prompt to write .vscode/mcp.json instead.',code:`code --add-mcp '{"name":"orva","type":"http","url":"${b.value}/mcp","headers":{"Authorization":"Bearer ${ee.value}"}}'`},{label:"Codex CLI",lang:"bash",note:"OpenAI's `codex` CLI. Writes to ~/.codex/config.toml.",code:`codex mcp add --transport streamable-http orva ${b.value}/mcp --header "Authorization: Bearer ${ee.value}"`},{label:"OpenCode",lang:"bash",note:`Interactive add. Pick "Remote", paste ${b.value}/mcp, then add the header Authorization: Bearer ${ee.value}.`,code:"opencode mcp add"},{label:"Zed",lang:"json",note:"Zed runs MCP as stdio subprocesses, so use the `mcp-remote` bridge. Paste under context_servers in ~/.config/zed/settings.json. Restart Zed.",code:`{ - "context_servers": { - "orva": { - "source": "custom", - "command": "npx", - "args": [ - "-y", "mcp-remote", - "${b.value}/mcp", - "--header", "Authorization:Bearer ${ee.value}" - ] - } - } -}`},{label:"Windsurf",lang:"json",note:"Paste into ~/.codeium/windsurf/mcp_config.json and reload Windsurf.",code:`{ - "mcpServers": { - "orva": { - "serverUrl": "${b.value}/mcp", - "headers": { - "Authorization": "Bearer ${ee.value}" - } - } - } -}`},{label:"claude.ai web",lang:"text",note:"UI-only flow. Settings → Connectors → Add custom connector. claude.ai opens an Orva login + consent popup and issues an OAuth 2.1 token automatically; no token paste required.",code:`URL: ${b.value}/mcp -Auth: OAuth (auto-discovered)`},{label:"ChatGPT",lang:"text",note:"UI-only flow. Settings → Apps & Connectors → Developer mode → Add new connector. ChatGPT discovers OIDC metadata, performs Dynamic Client Registration, and pops the Orva consent screen. No token paste required.",code:`URL: ${b.value}/mcp -Auth: OAuth (auto-discovered)`}]),ze=V(()=>{const m=JSON.stringify({url:b.value+"/mcp",headers:{Authorization:"Bearer "+ee.value}});return typeof window.btoa=="function"?window.btoa(m):m}),ct=V(()=>[{label:"Cursor (global)",lang:"json",note:"Paste into ~/.cursor/mcp.json, or .cursor/mcp.json in your project root for a per-workspace install.",code:`{ - "mcpServers": { - "orva": { - "url": "${b.value}/mcp", - "headers": { - "Authorization": "Bearer ${ee.value}" - } - } - } -}`},{label:"Cline",lang:"json",note:"In VS Code: open Cline → MCP icon → Configure MCP Servers. Cline writes cline_mcp_settings.json.",code:`{ - "mcpServers": { - "orva": { - "url": "${b.value}/mcp", - "headers": { - "Authorization": "Bearer ${ee.value}" - }, - "disabled": false - } - } -}`}]),K=Re({name:"CodeBlock",props:{code:{type:String,required:!0},lang:{type:String,default:""}},setup(m){const s=_e(!1),x=async()=>{await Ht(m.code)&&(s.value=!0,setTimeout(()=>{s.value=!1},1200))},u=V(()=>{const I=(m.lang||"").toLowerCase();if(I&&ve.getLanguage(I))try{return ve.highlight(m.code,{language:I,ignoreIllegals:!0}).value}catch{}return m.code.replace(/&/g,"&").replace(//g,">")});return()=>a("div",{class:"codeblock"},[a("div",{class:"codeblock-bar"},[a("span",{class:"codeblock-lang"},m.lang||""),a("button",{class:"codeblock-copy",onClick:x,title:"Copy code"},[s.value?a(ft,{class:"w-3 h-3"}):a(mt,{class:"w-3 h-3"}),s.value?"Copied":"Copy"])]),a("pre",{class:"codeblock-pre"},[a("code",{class:`hljs language-${(m.lang||"text").toLowerCase()}`,innerHTML:u.value})])])}}),oe=Re({name:"TabbedCode",props:{tabs:{type:Array,required:!0},storageKey:{type:String,default:""}},setup(m){const s=(()=>{try{if(m.storageKey){const I=localStorage.getItem(m.storageKey);if(I&&m.tabs.some(le=>le.label===I))return I}}catch{}return m.tabs[0]?.label})(),x=_e(s),u=I=>{x.value=I;try{m.storageKey&&localStorage.setItem(m.storageKey,I)}catch{}};return()=>{const I=m.tabs.find(le=>le.label===x.value)||m.tabs[0];return a("div",{class:"tabbed"},[a("div",{class:"tabbed-tabs"},m.tabs.map(le=>a("button",{key:le.label,class:["tabbed-tab",{active:le.label===x.value}],onClick:()=>u(le.label)},le.label))),I.note?a("div",{class:"tabbed-note"},I.note):null,a(K,{code:I.code,lang:I.lang})])}}}),Me=Re({name:"Callout",props:{title:{type:String,default:""},icon:{type:[Object,Function],default:null}},setup(m,{slots:s}){return()=>a("div",{class:"callout"},[a("div",{class:"callout-head"},[m.icon?a(m.icon,{class:"callout-icon"}):null,m.title?a("span",null,m.title):null]),a("div",{class:"callout-body"},s.default?.())])}});return(m,s)=>{const x=Rs("router-link");return q(),ue("div",qs,[t("header",Xs,[s[3]||(s[3]=t("div",{class:"docs-hero-bg","aria-hidden":"true"},null,-1)),t("div",Ws,[t("div",Vs,[s[1]||(s[1]=t("div",{class:"docs-hero-text"},[t("h1",{class:"docs-hero-title"}," Documentation "),t("p",{class:"docs-hero-sub"}," Everything you need to write, deploy, and operate functions on Orva. Handler contract, deploy + invoke, SDK, MCP, tracing, error taxonomy. ")],-1)),t("div",Ys,[t("button",{class:Ge(["docs-hero-copy-icon",{copied:J.value}]),title:J.value?"Copied":"Copy entire docs page as Markdown","aria-label":J.value?"Markdown copied to clipboard":"Copy entire docs page as Markdown",onClick:xe},[J.value?(q(),qe(f(ft),{key:0,class:"w-4 h-4"})):(q(),qe(f(mt),{key:1,class:"w-4 h-4"}))],10,Zs)])]),t("nav",Js,[s[2]||(s[2]=t("span",{class:"docs-hero-toc-label"},"Jump to",-1)),(q(),ue(De,null,Be(k,u=>t("a",{key:u.id,href:`#${u.id}`,class:Ge(["docs-hero-toc-link",{active:X.value===u.id}])},[t("span",en,R(u.num),1),t("span",null,R(u.label),1)],10,Qs)),64))])])]),t("section",tn,[s[5]||(s[5]=t("div",{class:"doc-section-head"},[t("span",{class:"doc-section-num"},"01"),t("div",null,[t("h2",{class:"doc-section-title"}," Handler contract "),t("p",{class:"doc-lede"}," One exported function receives the inbound HTTP event and returns an HTTP-shaped response. The adapter handles serialization and headers. ")])],-1)),w(f(oe),{tabs:B.value,"storage-key":"docs.handler"},null,8,["tabs"]),s[6]||(s[6]=ae('
Event shape
methodpathheadersquerybody
Response
{ statusCode, headers, body }

Non-string bodies are JSON-encoded by the adapter.

Runtime env
Env vars and secrets land in process.env / os.environ.
',1)),t("div",sn,[t("table",nn,[s[4]||(s[4]=t("thead",null,[t("tr",null,[t("th",null,"Runtime"),t("th",null,"ID"),t("th",{class:"hidden sm:table-cell"}," Entrypoint "),t("th",{class:"hidden md:table-cell"}," Dependencies ")])],-1)),t("tbody",null,[(q(),ue(De,null,Be(re,u=>t("tr",{key:u.id},[t("td",on,[(q(),qe(jt(u.icon),{class:"shrink-0"})),g(" "+R(u.name),1)]),t("td",an,R(u.id),1),t("td",rn,R(u.entry),1),t("td",cn,R(u.deps),1)])),64))])])])]),t("section",ln,[s[11]||(s[11]=ae('
02

Deploy & invoke

The dashboard handles day-to-day work; these calls are for CI and automation. Builds run async; poll /api/v1/deployments/<id> or stream /api/v1/deployments/<id>/stream until phase: done.

',1)),w(f(L)),t("div",dn,[t("div",un,[s[7]||(s[7]=t("div",{class:"doc-step-label"},[t("span",{class:"doc-step-num"},"1"),g(" Create the function row ")],-1)),w(f(K),{code:ke.value,lang:"bash"},null,8,["code"])]),t("div",pn,[s[8]||(s[8]=t("div",{class:"doc-step-label"},[t("span",{class:"doc-step-num"},"2"),g(" Upload code ")],-1)),w(f(K),{code:Se.value,lang:"bash"},null,8,["code"])])]),t("div",hn,[s[9]||(s[9]=t("div",{class:"doc-microlabel"}," Invoke ",-1)),w(f(oe),{tabs:pe.value,"storage-key":"docs.invoke"},null,8,["tabs"])]),w(f(Me),{icon:f(vt),title:"Custom routes"},{default:Oe(()=>[...s[10]||(s[10]=[g(" Attach a friendly path with ",-1),t("code",{class:"doc-chip"},"POST /api/v1/routes",-1),g(". Reserved prefixes: ",-1),t("code",{class:"doc-chip"},"/api/",-1),t("code",{class:"doc-chip"},"/fn/",-1),t("code",{class:"doc-chip"},"/mcp/",-1),t("code",{class:"doc-chip"},"/web/",-1),t("code",{class:"doc-chip"},"/_orva/",-1),g(". ",-1)])]),_:1},8,["icon"])]),t("section",gn,[s[15]||(s[15]=t("div",{class:"doc-section-head"},[t("span",{class:"doc-section-num"},"03"),t("div",null,[t("h2",{class:"doc-section-title"}," Configuration reference "),t("p",{class:"doc-lede"}," Everything below lives on the function record. Secrets are stored encrypted and only decrypt into the worker environment at spawn time. ")])],-1)),t("div",bn,[t("table",fn,[s[12]||(s[12]=t("thead",null,[t("tr",null,[t("th",null,"Field"),t("th",{class:"hidden sm:table-cell"}," Purpose "),t("th",null,"Behaviour")])],-1)),t("tbody",null,[(q(),ue(De,null,Be(he,u=>t("tr",{key:u.field,class:"align-top"},[t("td",mn,[(q(),qe(jt(u.icon),{class:Ge(["w-3.5 h-3.5 shrink-0",u.iconClass])},null,8,["class"])),t("code",null,R(u.field),1)]),t("td",vn,R(u.purpose),1),t("td",yn,R(u.body),1)])),64))])])]),t("div",En,[s[13]||(s[13]=t("div",{class:"doc-microlabel"}," Set a secret ",-1)),w(f(K),{code:ce.value,lang:"bash"},null,8,["code"])]),t("details",wn,[t("summary",_n,[w(f(Ut),{class:"w-3.5 h-3.5 transition-transform group-open:rotate-90 text-foreground-muted"}),s[14]||(s[14]=g(" Signed-invoke recipe (HMAC, opt-in) ",-1))]),t("div",kn,[w(f(K),{code:ye.value,lang:"bash"},null,8,["code"])])])]),t("section",Sn,[s[21]||(s[21]=ae('
04

SDK from inside a function

The bundled orva module exposes three primitives every function can use without extra dependencies: a per-function key/value store, in-process calls to other Orva functions, and a fire-and-forget background job queue. Routes through the per-process internal token injected at worker spawn time.

orva.kv
put / get / delete / list

Per-function namespace on SQLite, optional TTL.

orva.invoke
invoke(name, payload)

In-process call to another function. 8-deep call cap.

orva.jobs
jobs.enqueue(name, payload)

Fire-and-forget; persisted; retried with exp backoff.

',2)),t("div",Tn,[s[16]||(s[16]=t("div",{class:"doc-microlabel"}," KV: get/put with TTL ",-1)),w(f(oe),{tabs:Te,"storage-key":"docs.sdk.kv"}),s[17]||(s[17]=ae('

Browse / inspect / edit / delete / set keys without leaving the dashboard at /web/functions/<name>/kv (or click the KV button in the editor's action bar). REST mirror at GET/PUT/DELETE /api/v1/functions/<id>/kv[/<key>]; MCP tools kv_list / kv_get / kv_put / kv_delete for agents.

',1))]),t("div",xn,[s[18]||(s[18]=t("div",{class:"doc-microlabel"}," Function-to-function: invoke() ",-1)),w(f(oe),{tabs:$e,"storage-key":"docs.sdk.invoke"})]),t("div",An,[s[19]||(s[19]=t("div",{class:"doc-microlabel"}," Background jobs: jobs.enqueue() ",-1)),w(f(oe),{tabs:je,"storage-key":"docs.sdk.jobs"})]),w(f(Me),{icon:f(vt),title:"Network mode"},{default:Oe(()=>[...s[20]||(s[20]=[g(" The SDK reaches orvad over loopback through the host gateway, so the function needs ",-1),t("code",{class:"doc-chip"},'network_mode: "egress"',-1),g(". On the default ",-1),t("code",{class:"doc-chip"},'"none"',-1),g(" the SDK throws ",-1),t("code",{class:"doc-chip"},"OrvaUnavailableError",-1),g(" with a clear hint. ",-1)])]),_:1},8,["icon"])]),t("section",Cn,[t("div",On,[s[32]||(s[32]=t("span",{class:"doc-section-num"},"05",-1)),t("div",null,[s[31]||(s[31]=t("h2",{class:"doc-section-title"}," Schedules ",-1)),t("p",Rn,[s[23]||(s[23]=g(" Fire any function on a cron expression. The scheduler runs as part of the orvad process; no external service. Manage from the ",-1)),w(x,{to:"/cron",class:"text-foreground hover:text-white underline decoration-dotted underline-offset-4"},{default:Oe(()=>[...s[22]||(s[22]=[g("Schedules page",-1)])]),_:1}),s[24]||(s[24]=g(" or via the API. Standard 5-field cron with the usual shorthands (",-1)),s[25]||(s[25]=t("code",{class:"doc-chip"},"@daily",-1)),s[26]||(s[26]=g(", ",-1)),s[27]||(s[27]=t("code",{class:"doc-chip"},"@hourly",-1)),s[28]||(s[28]=g(", ",-1)),s[29]||(s[29]=t("code",{class:"doc-chip"},"*/5 * * * *",-1)),s[30]||(s[30]=g("). ",-1))])])]),w(f(oe),{tabs:He.value,"storage-key":"docs.cron"},null,8,["tabs"]),w(f(Me),{icon:f(Cs),title:"Cron-fired headers"},{default:Oe(()=>[...s[33]||(s[33]=[g(" Every cron-triggered invocation arrives at the function with ",-1),t("code",{class:"doc-chip"},"x-orva-trigger: cron",-1),g(" and ",-1),t("code",{class:"doc-chip"},"x-orva-cron-id: cron_…",-1),g(" on the event headers, so user code can branch on origin. ",-1)])]),_:1},8,["icon"])]),t("section",Nn,[t("div",In,[s[38]||(s[38]=t("span",{class:"doc-section-num"},"06",-1)),t("div",null,[s[37]||(s[37]=t("h2",{class:"doc-section-title"}," Webhooks ",-1)),t("p",Mn,[s[35]||(s[35]=g(" Operator-managed subscriptions for system events. Configure URLs from the ",-1)),w(x,{to:"/webhooks",class:"text-foreground hover:text-white underline decoration-dotted underline-offset-4"},{default:Oe(()=>[...s[34]||(s[34]=[g("Webhooks page",-1)])]),_:1}),s[36]||(s[36]=g("; Orva delivers signed POSTs to them when matching events fire (deployments, function lifecycle, cron failures, job outcomes). Subscriptions are global, not per-function. ",-1))])])]),w(f(Z)),s[41]||(s[41]=ae('
Headers
X-Orva-EventX-Orva-Delivery-IdX-Orva-TimestampX-Orva-Signature
Signature
sha256=hex(hmac(secret, ts.body))

Same shape as Stripe / signed-invoke. Receivers verify with the secret returned at create time.

Retries
5 attemptsexp backoff (≤ 1h)

Receiver must 2xx within 15s.

',1)),t("div",Pn,[t("table",Ln,[s[39]||(s[39]=t("thead",null,[t("tr",null,[t("th",null,"Event"),t("th",null,"When it fires")])],-1)),t("tbody",null,[(q(),ue(De,null,Be(Ue,u=>t("tr",{key:u.name},[t("td",Dn,[t("code",null,R(u.name),1)]),t("td",Bn,R(u.when),1)])),64))])])]),t("div",Hn,[s[40]||(s[40]=t("div",{class:"doc-microlabel"}," Verify a delivery ",-1)),w(f(oe),{tabs:Xe,"storage-key":"docs.webhooks.verify"})])]),t("section",$n,[s[51]||(s[51]=t("div",{class:"doc-section-head"},[t("span",{class:"doc-section-num"},"07"),t("div",null,[t("h2",{class:"doc-section-title"}," MCP: Model Context Protocol "),t("p",{class:"doc-lede"}," Same API surface the dashboard uses, exposed as 70 tools an agent can call directly. API key permissions scope the available tool set. ")])],-1)),t("div",jn,[t("div",Un,[s[42]||(s[42]=t("div",{class:"doc-microlabel"}," Endpoint ",-1)),t("div",zn,[t("code",Kn,R(b.value)+"/mcp",1)])]),s[43]||(s[43]=ae('
Auth header
Authorization: Bearer <token>

Or as a fallback: X-Orva-API-Key: <token>

Transport
Streamable HTTPMCP 2025-11-25
',2))]),w(f(Me),{icon:f(Ze),title:"Two header formats; same auth"},{default:Oe(()=>[...s[44]||(s[44]=[g(" Either header works against the same API key store with identical permission gating. ",-1),t("code",{class:"doc-chip"},"Authorization: Bearer",-1),g(" is the MCP / OAuth 2 spec form; every MCP SDK (Claude Code, Claude Desktop, Cursor, mcp-remote, Python ",-1),t("code",{class:"doc-chip"},"mcp",-1),g(") configures it natively, so prefer it for new setups. ",-1),t("code",{class:"doc-chip"},"X-Orva-API-Key",-1),g(" is the same header the REST API accepts; useful when a tool reuses an existing Orva REST integration. Internally both paths SHA-256-hash the token and look it up against the same ",-1),t("code",{class:"doc-chip"},"api_keys",-1),g(" table. ",-1)])]),_:1},8,["icon"]),t("div",Fn,[t("div",Gn,[w(f(Ze),{class:"w-4 h-4 shrink-0 text-foreground-muted"}),be.value?(q(),ue("span",Xn,[s[47]||(s[47]=g(" Token minted: ",-1)),t("code",Wn,R(at.value)+"…",1),s[48]||(s[48]=g(" Shown once, copy now. ",-1))])):(q(),ue("span",qn,[s[45]||(s[45]=g(" Snippets show ",-1)),t("code",{class:"doc-chip"},R(Gt)),s[46]||(s[46]=g(". Mint a token to substitute it everywhere. ",-1))]))]),t("button",{class:"doc-token-btn",disabled:Ie.value,onClick:it},[w(f(Ze),{class:"w-3.5 h-3.5"}),g(" "+R(be.value?"Mint another":Ie.value?"Minting…":"Generate token"),1)],8,Vn)]),w(f(oe),{tabs:rt.value,"storage-key":"docs.mcp.install"},null,8,["tabs"]),t("details",Yn,[t("summary",Zn,[w(f(Ut),{class:"w-3.5 h-3.5 transition-transform group-open:rotate-90 text-foreground-muted"}),s[49]||(s[49]=g(" More clients (Cursor, VS Code, Codex CLI, OpenCode, Zed, Windsurf, ChatGPT, manual config) ",-1))]),t("div",Jn,[w(f(oe),{tabs:wt.value,"storage-key":"docs.mcp.install.more"},null,8,["tabs"]),s[50]||(s[50]=t("div",{class:"doc-microlabel pt-1"}," Hand-edited config files ",-1)),w(f(oe),{tabs:ct.value,"storage-key":"docs.mcp.manual"},null,8,["tabs"])])])]),t("section",Qn,[s[52]||(s[52]=ae('
08

System prompt for AI assistants

Paste the prompt below into ChatGPT, Claude, Gemini, Cursor, Copilot, or any other AI tool to teach it Orva's full surface Handler contract, runtimes, sandbox limits, the in-sandbox orva SDK (kv / invoke / jobs), cron triggers, system-event webhooks, auth modes, and production patterns. The model then turns "describe what I want" into a pasteable handler on the first try.

',1)),t("div",eo,[t("button",{class:Ge(["ai-copy-btn",{copied:N.value}]),onClick:U},[N.value?(q(),qe(f(ft),{key:0,class:"w-3.5 h-3.5"})):(q(),qe(f(mt),{key:1,class:"w-3.5 h-3.5"})),g(" "+R(N.value?"Copied":"Copy system prompt"),1)],2)]),t("div",{class:Ge(["prompt-collapse",{expanded:te.value}])},[w(f(K),{code:f(ne),lang:"text"},null,8,["code"]),te.value?Os("",!0):(q(),ue("div",to))],2),t("button",{class:"prompt-expand-btn","aria-expanded":te.value,onClick:s[0]||(s[0]=u=>te.value=!te.value)},[w(f(Ms),{class:Ge(["w-3.5 h-3.5 transition-transform",{"rotate-180":te.value}])},null,8,["class"]),g(" "+R(te.value?"Collapse system prompt":"Expand full system prompt (~400 lines)"),1)],8,so)]),t("section",no,[s[54]||(s[54]=ae('
09

Tracing

Every invocation chain is recorded as a causal trace. automatically, with zero changes to your function code. HTTP requests, F2F invokes, jobs, cron, inbound webhooks, and replays all stitch into the same tree. The dashboard renders it as a waterfall at /traces.

Each execution row IS a span. Spans share a trace_id; child spans point at their parent via parent_span_id. You don't instantiate spans, you don't import a tracer; you just write your handler and the platform plumbs IDs through every internal hop.

',2)),w(f(ie)),s[55]||(s[55]=t("h3",{class:"doc-h3"},"What user code sees",-1)),s[56]||(s[56]=t("p",{class:"doc-prose"}," Two env vars are stamped per invocation. Read them only if you want to log the trace_id alongside your own messages; they're optional. ",-1)),w(f(K),{code:xo,lang:"text"}),s[57]||(s[57]=t("h3",{class:"doc-h3"},"Automatic propagation",-1)),s[58]||(s[58]=t("p",{class:"doc-prose"},[g(" When a function calls another via the SDK, the trace context flows through automatically. The called function becomes a child span of the caller; both share the same "),t("code",{class:"doc-chip"},"trace_id"),g(". ")],-1)),w(f(K),{code:Ao,lang:"js"}),s[59]||(s[59]=ae('

Job enqueues work the same way: orva.jobs.enqueue() records the trace context on the job row. When the scheduler picks the job up later, the resulting execution lands in the same trace as the function that enqueued it, even if the gap is hours or days.

Triggers

Each span carries a trigger label so the UI can show how the chain started.

',3)),t("div",oo,[t("table",ao,[s[53]||(s[53]=t("thead",null,[t("tr",null,[t("th",null,"Trigger"),t("th",null,"Meaning")])],-1)),t("tbody",null,[(q(),ue(De,null,Be(We,u=>t("tr",{key:u.name},[t("td",io,[t("code",null,R(u.name),1)]),t("td",ro,R(u.desc),1)])),64))])])]),s[60]||(s[60]=t("h3",{class:"doc-h3"},"External correlation (W3C traceparent)",-1)),s[61]||(s[61]=t("p",{class:"doc-prose"},[g(" Send a standard "),t("code",{class:"doc-chip"},"traceparent"),g(" header on the inbound HTTP request and Orva makes its trace a child of yours. The same trace_id is echoed back as "),t("code",{class:"doc-chip"},"X-Trace-Id"),g(" on every response, so external systems can correlate without parsing bodies. ")],-1)),w(f(K),{code:Co,lang:"bash"}),s[62]||(s[62]=ae('

Outlier detection

Each function maintains an in-memory rolling P95 baseline over its last 100 successful warm executions. An invocation is flagged as an outlier when it has at least 20 baseline samples AND its duration exceeds P95 × 2. Cold starts and errors are excluded from the baseline so a flapping function can't drag it down. The flag and baseline P95 are stored on the execution row and rendered as an amber flag icon next to the span.

Where to look

  • /traces: list of recent traces, filterable by function / status / outlier-only.
  • /traces/:id: waterfall + per-span detail. Click a span to jump to its execution in the Invocations log.
  • GET /api/v1/traces/{id}: full span tree as JSON. Pair with list_traces / get_trace MCP tools for AI agents.
  • GET /api/v1/functions/{id}/baseline: current P95/P99/mean for a function.
',4))]),t("section",co,[s[64]||(s[64]=ae('
10

Errors & recovery

Every error response uses the same envelope so log scrapers and retries can match on code. Deploys are content-addressed; rollback retargets the active version pointer and refreshes warm workers.

',1)),w(f(K),{code:Oo,lang:"json"}),t("div",lo,[t("table",uo,[s[63]||(s[63]=t("thead",null,[t("tr",null,[t("th",null,"Code"),t("th",null,"When you see it")])],-1)),t("tbody",null,[(q(),ue(De,null,Be(Ee,u=>t("tr",{key:u.code},[t("td",po,[t("code",null,R(u.code),1)]),t("td",ho,R(u.when),1)])),64))])])])]),t("section",go,[s[81]||(s[81]=ae('
11

CLI

orva is a single static binary that talks to a remote (or local) Orva server over HTTPS. Same binary as the daemon, orva serve starts a server, every other subcommand is a CLI client. Drop it on operator laptops, CI runners, or anywhere bash runs.

Install (server included)
curl … install.sh | sh

Full install: daemon + nsjail + rootfs + CLI.

Install (CLI only)
install.sh --cli-only

~10 MB binary at /usr/local/bin/orva. No service.

Inside Docker
docker exec orva orva …

CLI auto-authed via ~/.orva/config.yaml.

Authenticate

',3)),t("p",bo,[s[66]||(s[66]=g(" The CLI reads ",-1)),s[67]||(s[67]=t("code",{class:"doc-chip"},"~/.orva/config.yaml",-1)),s[68]||(s[68]=g(" for ",-1)),s[69]||(s[69]=t("code",{class:"doc-chip"},"endpoint",-1)),s[70]||(s[70]=g(" + ",-1)),s[71]||(s[71]=t("code",{class:"doc-chip"},"api_key",-1)),s[72]||(s[72]=g(". Generate a key from ",-1)),w(x,{to:"/api-keys",class:"text-foreground hover:text-white underline decoration-dotted underline-offset-4"},{default:Oe(()=>[...s[65]||(s[65]=[g("Keys",-1)])]),_:1}),s[73]||(s[73]=g(" in the dashboard, then: ",-1))]),w(f(K),{code:Ro,lang:"bash"}),s[82]||(s[82]=t("h3",{class:"doc-h3"},"Command index",-1)),t("div",fo,[t("table",mo,[s[74]||(s[74]=t("thead",null,[t("tr",null,[t("th",null,"Command"),t("th",null,"Subcommands"),t("th",{class:"hidden md:table-cell"},"Purpose")])],-1)),t("tbody",null,[(q(),ue(De,null,Be(Ve,u=>t("tr",{key:u.cmd},[t("td",vo,[t("code",null,"orva "+R(u.cmd),1)]),t("td",yo,R(u.subs),1),t("td",Eo,R(u.purpose),1)])),64))])])]),s[83]||(s[83]=t("h3",{class:"doc-h3"},"Common recipes",-1)),t("div",wo,[s[75]||(s[75]=t("div",{class:"doc-microlabel"},"Deploy a function from a directory",-1)),w(f(K),{code:No,lang:"bash"})]),t("div",_o,[s[76]||(s[76]=t("div",{class:"doc-microlabel"},"Invoke + tail logs",-1)),w(f(K),{code:Io,lang:"bash"})]),t("div",ko,[s[77]||(s[77]=t("div",{class:"doc-microlabel"},"Manage KV state",-1)),w(f(K),{code:Mo,lang:"bash"})]),t("div",So,[s[78]||(s[78]=t("div",{class:"doc-microlabel"},"Secrets, cron, jobs, webhooks",-1)),w(f(K),{code:Po,lang:"bash"})]),t("div",To,[s[79]||(s[79]=t("div",{class:"doc-microlabel"},"System health, metrics, vacuum",-1)),w(f(K),{code:Lo,lang:"bash"})]),w(f(Me),{icon:f(Ze),title:"Shell completion"},{default:Oe(()=>[...s[80]||(s[80]=[g(" Generate completion for your shell: ",-1),t("code",{class:"doc-chip"},"orva completion bash | sudo tee /etc/bash_completion.d/orva",-1),g(", or ",-1),t("code",{class:"doc-chip"},"zsh",-1),g(" / ",-1),t("code",{class:"doc-chip"},"fish",-1),g(" / ",-1),t("code",{class:"doc-chip"},"powershell",-1),g(". Tab-completes commands, subcommands, and flags. ",-1)])]),_:1},8,["icon"])])])}}};export{Wo as default}; diff --git a/backend/internal/server/ui_dist/assets/Docs-Dwszf_vd.css b/backend/internal/server/ui_dist/assets/Docs-Dwszf_vd.css deleted file mode 100644 index f787144..0000000 --- a/backend/internal/server/ui_dist/assets/Docs-Dwszf_vd.css +++ /dev/null @@ -1 +0,0 @@ -pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-variable,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id{color:#79c0ff}.hljs-regexp,.hljs-string,.hljs-meta .hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-comment,.hljs-code,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-tag,.hljs-selector-pseudo{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}.docs-hero{position:relative;border:1px solid var(--color-border);border-radius:.85rem;overflow:hidden;background:radial-gradient(ellipse 60% 80% at 0% 0%,#553f831f,#553f8300 60%),linear-gradient(180deg,rgba(255,255,255,.012) 0%,transparent 100%),var(--color-background)}.docs-hero-bg{position:absolute;inset:0;pointer-events:none;background-image:radial-gradient(rgba(255,255,255,.025) 1px,transparent 1px);background-size:14px 14px;background-position:0 0;mask-image:linear-gradient(180deg,black 0%,transparent 100%);-webkit-mask-image:linear-gradient(180deg,black 0%,transparent 100%)}.docs-hero-content{position:relative;padding:1.6rem 1.5rem 1.2rem;display:flex;flex-direction:column;gap:1rem}@media(min-width:640px){.docs-hero-content{padding:2.2rem 2.2rem 1.6rem;gap:1.4rem}}.docs-hero-eyebrow{display:inline-flex;align-items:center;gap:.55rem}.docs-hero-eyebrow-mark{display:inline-block;width:6px;height:6px;border-radius:999px;background:var(--color-primary);box-shadow:0 0 12px #553f8399}.docs-hero-eyebrow-label{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.2em;text-transform:uppercase;color:var(--color-foreground-muted)}.docs-hero-row{display:flex;flex-direction:column;gap:1rem;align-items:stretch}@media(min-width:768px){.docs-hero-row{flex-direction:row;align-items:flex-start;justify-content:space-between;gap:2rem}}.docs-hero-text{max-width:56ch}.docs-hero-title{margin:0;font-family:var(--font-sans);font-size:30px;font-weight:600;letter-spacing:-.02em;line-height:1.05;color:var(--color-foreground)}@media(min-width:640px){.docs-hero-title{font-size:38px}}.docs-hero-sub{margin:.6rem 0 0;font-family:var(--font-sans);font-size:13.5px;line-height:1.55;color:var(--color-foreground-muted)}.docs-hero-actions{display:flex;align-items:flex-start;flex-shrink:0}.docs-hero-copy-icon{display:inline-flex;align-items:center;justify-content:center;width:2.2rem;height:2.2rem;border-radius:.45rem;border:1px solid var(--color-border);background:var(--color-surface);color:var(--color-foreground-muted);cursor:pointer;transition:color .12s,border-color .12s,background-color .12s}.docs-hero-copy-icon:hover{color:var(--color-foreground);border-color:var(--color-foreground-muted);background:var(--color-surface-hover, rgba(255, 255, 255, .04))}.docs-hero-copy-icon:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.docs-hero-copy-icon.copied{color:var(--color-success-fg);border-color:#4caf5073;background:#4caf5014}.docs-hero-copy{display:inline-flex;align-items:center;gap:.45rem;padding:.55rem .9rem;border-radius:.5rem;border:1px solid var(--color-border);background:var(--color-surface);color:var(--color-foreground);font-family:var(--font-sans);font-size:12.5px;font-weight:500;cursor:pointer;transition:background-color .12s,border-color .12s,color .12s}.docs-hero-copy:hover{background:var(--color-surface-hover, rgba(255, 255, 255, .04));border-color:var(--color-foreground-muted)}.docs-hero-copy:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.docs-hero-copy.copied{border-color:#4caf5066;color:var(--color-success-fg);background:#4caf5014}.docs-hero-toc{display:flex;flex-wrap:wrap;align-items:center;gap:.4rem;padding-top:1rem;border-top:1px dashed rgba(255,255,255,.05)}.docs-hero-toc-label{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.18em;text-transform:uppercase;color:var(--color-foreground-muted);margin-right:.3rem}.docs-hero-toc-link{display:inline-flex;align-items:center;gap:.4rem;padding:.32rem .6rem;border-radius:.4rem;border:1px solid var(--color-border);background:var(--color-surface);color:var(--color-foreground-muted);font-family:var(--font-sans);font-size:11.5px;font-weight:500;text-decoration:none;transition:color .12s,border-color .12s,background-color .12s}.docs-hero-toc-link:hover{color:var(--color-foreground);border-color:var(--color-foreground-muted)}.docs-hero-toc-link.active{color:var(--color-foreground);border-color:var(--color-primary);background:#553f832e}.docs-hero-toc-num{font-family:var(--font-mono);font-size:10px;letter-spacing:.04em;color:var(--color-foreground-muted);opacity:.7}.docs-hero-toc-link.active .docs-hero-toc-num{color:#ffffffd9;opacity:1}.doc-diagram{margin:0 0 .75rem;padding:1.2rem 1.2rem 1.4rem;border:1px solid var(--color-border);border-radius:.7rem;background:radial-gradient(ellipse 50% 80% at 100% 0%,rgba(85,63,131,.06) 0%,transparent 70%),var(--color-background)}.doc-diagram-cap{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.16em;text-transform:uppercase;color:var(--color-foreground-muted);margin:0 0 1rem}.doc-pipeline{display:flex;align-items:stretch;justify-content:center;gap:.4rem;overflow-x:auto;padding-bottom:.4rem}.doc-pipeline-stage{display:flex;align-items:center;gap:.6rem;padding:.65rem .85rem;border:1px solid var(--color-border);border-radius:.55rem;background:var(--color-surface);min-width:0;flex-shrink:0}.doc-pipeline-glyph{display:inline-flex;align-items:center;justify-content:center;width:1.6rem;height:1.6rem;border-radius:.4rem;background:#553f832e;border:1px solid rgba(85,63,131,.45);color:var(--color-foreground);font-family:var(--font-mono);font-size:13px;line-height:1}.doc-pipeline-label{display:flex;flex-direction:column;gap:.05rem;min-width:0}.doc-pipeline-name{font-family:var(--font-sans);font-size:12.5px;font-weight:600;color:var(--color-foreground);white-space:nowrap}.doc-pipeline-sub{font-family:var(--font-mono);font-size:10px;color:var(--color-foreground-muted);white-space:nowrap}.doc-pipeline-arrow{flex-shrink:0;align-self:center;width:1.6rem;height:1px;background:linear-gradient(90deg,#ffffff0d,#ffffff2e);position:relative}.doc-pipeline-arrow:after{content:"";position:absolute;right:-2px;top:50%;width:.42rem;height:.42rem;border-top:1px solid rgba(255,255,255,.35);border-right:1px solid rgba(255,255,255,.35);transform:translateY(-50%) rotate(45deg)}.doc-trace{display:flex;flex-direction:column;gap:.55rem}.doc-trace-axis{display:flex;justify-content:space-between;font-family:var(--font-mono);font-size:10px;color:var(--color-foreground-muted);padding:0 0 .2rem;border-bottom:1px dashed rgba(255,255,255,.05)}.doc-trace-row{display:grid;grid-template-columns:11rem 1fr 3rem;align-items:center;gap:.7rem;font-family:var(--font-sans)}.doc-trace-row.is-child .doc-trace-label,.doc-trace-row.is-grand .doc-trace-label{position:relative}.doc-trace-row.is-child .doc-trace-label:before,.doc-trace-row.is-grand .doc-trace-label:before{content:"└";position:absolute;left:-.7rem;top:.05rem;color:var(--color-foreground-muted);font-family:var(--font-mono);opacity:.55}.doc-trace-row.is-child .doc-trace-label,.doc-trace-row.is-grand .doc-trace-label{padding-left:1rem}.doc-trace-label{display:flex;align-items:center;gap:.45rem;min-width:0}.doc-trace-fn{font-size:12.5px;color:var(--color-foreground);font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.doc-trace-trigger{font-family:var(--font-mono);font-size:9.5px;letter-spacing:.06em;text-transform:lowercase;color:var(--color-foreground-muted);border:1px solid var(--color-border);border-radius:.25rem;padding:.05rem .35rem;background:var(--color-background)}.doc-trace-track{position:relative;height:10px;background:#ffffff06;border-radius:999px;overflow:hidden}.doc-trace-bar{position:absolute;top:1px;bottom:1px;background:linear-gradient(90deg,#553f83d9,#553f838c);border-radius:999px;box-shadow:0 0 12px #553f8340}.doc-trace-row.is-child .doc-trace-bar{background:linear-gradient(90deg,#60a5facc,#60a5fa80);box-shadow:0 0 10px #60a5fa40}.doc-trace-row.is-grand .doc-trace-bar{background:linear-gradient(90deg,#34d399cc,#34d39980);box-shadow:0 0 10px #34d39940}.doc-trace-dur{font-family:var(--font-mono);font-size:10.5px;color:var(--color-foreground);text-align:right}.doc-trace-legend{font-family:var(--font-sans);font-size:11.5px;color:var(--color-foreground-muted);padding-top:.4rem;border-top:1px dashed rgba(255,255,255,.05)}.doc-webhook{display:grid;grid-template-columns:1fr;gap:.7rem;align-items:stretch}@media(min-width:768px){.doc-webhook{grid-template-columns:1fr 1.4fr 1fr}}.doc-webhook-actor{display:flex;flex-direction:column;gap:.4rem;padding:.85rem;border:1px solid var(--color-border);border-radius:.55rem;background:var(--color-surface)}.doc-webhook-actor-head{font-family:var(--font-mono);font-size:11px;font-weight:600;color:var(--color-foreground);letter-spacing:.06em}.doc-webhook-actor-body{display:flex;flex-direction:column;gap:.3rem;font-family:var(--font-sans);font-size:11.5px;color:var(--color-foreground-muted)}.doc-webhook-wire{position:relative;display:flex;flex-direction:column;align-items:stretch;justify-content:center;padding:.85rem .7rem;border:1px dashed rgba(85,63,131,.45);border-radius:.55rem;background:radial-gradient(ellipse 80% 100% at 50% 50%,rgba(85,63,131,.08) 0%,transparent 70%)}.doc-webhook-wire-line{display:none}.doc-webhook-wire-payload{display:flex;flex-direction:column;gap:.45rem;font-family:var(--font-sans);font-size:11.5px}.doc-webhook-wire-method{display:inline-flex;align-items:center;align-self:flex-start;padding:.15rem .55rem;border-radius:.3rem;background:#553f8340;border:1px solid rgba(85,63,131,.55);color:var(--color-foreground);font-family:var(--font-mono);font-size:10.5px;font-weight:600;letter-spacing:.08em}.doc-webhook-wire-headers{display:flex;flex-wrap:wrap;gap:.3rem}.doc-webhook-wire-headers code{font-family:var(--font-mono);font-size:10.5px;padding:.1rem .4rem;border-radius:.25rem;border:1px solid var(--color-border);background:var(--color-background);color:var(--color-foreground)}.doc-webhook-wire-sig{font-family:var(--font-mono);font-size:10.5px;color:var(--color-foreground-muted);padding-top:.2rem;border-top:1px dashed rgba(255,255,255,.05)}.prompt-collapse{position:relative;max-height:9.5rem;overflow:hidden;border-radius:.6rem;transition:max-height .28s ease}.prompt-collapse.expanded{max-height:7000px}.prompt-collapse-fade{position:absolute;inset:auto 0 0;height:4.5rem;pointer-events:none;background:linear-gradient(180deg,transparent 0%,var(--color-background) 85%);border-radius:0 0 .6rem .6rem}.prompt-expand-btn{display:inline-flex;align-items:center;gap:.4rem;margin-top:.5rem;padding:.45rem .85rem;border:1px solid var(--color-border);border-radius:.45rem;background:var(--color-surface);color:var(--color-foreground-muted);font-family:var(--font-sans);font-size:12px;font-weight:500;cursor:pointer;transition:color .12s,border-color .12s,background-color .12s}.prompt-expand-btn:hover{color:var(--color-foreground);border-color:var(--color-foreground-muted)}.prompt-expand-btn:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.doc-section-head{display:flex;align-items:flex-start;gap:.85rem}.doc-section-num{display:inline-flex;flex-shrink:0;align-items:center;justify-content:center;width:1.85rem;height:1.85rem;border-radius:.5rem;background:linear-gradient(135deg,var(--color-primary) 0%,var(--color-primary-hover) 100%);color:var(--color-primary-foreground);font-family:var(--font-mono);font-size:11px;font-weight:700;letter-spacing:.04em;box-shadow:0 0 0 1px #ffffff0a inset,0 6px 18px -8px #553f8399}.doc-section-title{font-family:var(--font-sans);font-size:1.05rem;line-height:1.25;font-weight:600;letter-spacing:-.01em;color:var(--color-foreground);margin:0}.doc-lede{font-family:var(--font-sans);font-size:13px;line-height:1.6;color:var(--color-foreground-muted);max-width:64ch;margin:.35rem 0 0}.doc-chip{display:inline-block;font-family:var(--font-mono);font-size:11.5px;line-height:1.4;padding:.1rem .4rem;margin:0 .05rem;border-radius:.3rem;background:var(--color-surface);border:1px solid var(--color-border);color:var(--color-foreground);white-space:nowrap;vertical-align:baseline}.doc-chip.break-all{white-space:normal;word-break:break-all}.doc-microlabel{font-family:var(--font-sans);font-size:12.5px;font-weight:600;letter-spacing:-.005em;color:var(--color-foreground);padding-left:.55rem;border-left:2px solid rgba(85,63,131,.55);text-transform:none}.doc-card .doc-microlabel{border-left:none;padding-left:0;font-size:12px;font-weight:600;color:var(--color-foreground)}.doc-card{position:relative;padding:.85rem .95rem;background:linear-gradient(180deg,rgba(255,255,255,.015) 0%,transparent 100%),var(--color-background);border:1px solid var(--color-border);border-radius:.6rem;display:flex;flex-direction:column;gap:.5rem;transition:border-color .16s}.doc-card:hover{border-color:#553f8399}.doc-card-body{font-family:var(--font-sans);font-size:13px;line-height:1.55;color:var(--color-foreground);display:flex;flex-wrap:wrap;gap:.3rem .35rem;align-items:center}.doc-card-body p{flex-basis:100%;margin:0;font-size:12.5px;line-height:1.55}.doc-step-label{display:flex;align-items:center;gap:.55rem;font-family:var(--font-sans);font-size:12.5px;font-weight:600;letter-spacing:-.005em;color:var(--color-foreground)}.doc-step-num{display:inline-flex;align-items:center;justify-content:center;width:1.1rem;height:1.1rem;border-radius:999px;background:#553f832e;border:1px solid rgba(85,63,131,.6);color:var(--color-foreground);font-size:10px;font-weight:700;letter-spacing:0}.doc-table-wrap{background:var(--color-background);border:1px solid var(--color-border);border-radius:.6rem;overflow:hidden}.doc-table{width:100%;border-collapse:collapse;font-family:var(--font-sans)}.doc-table thead{background:var(--color-surface);border-bottom:1px solid var(--color-border)}.doc-table thead th{text-align:left;padding:.7rem 1rem;font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--color-foreground-muted)}.doc-table tbody tr{border-top:1px solid var(--color-border);transition:background-color .12s}.doc-table tbody tr:first-child{border-top:0}.doc-table tbody tr:hover{background:#ffffff04}.doc-table td{padding:.75rem 1rem;vertical-align:top}.doc-cell-key{display:flex;align-items:center;gap:.5rem;font-family:var(--font-sans);font-size:13px;font-weight:500;color:var(--color-foreground)}.doc-cell-key code{font-family:var(--font-mono);font-size:12px;color:var(--color-foreground);font-weight:500}.doc-cell-mono{font-family:var(--font-mono);font-size:12px;color:var(--color-foreground-muted)}.doc-cell-body{font-family:var(--font-sans);font-size:12.5px;line-height:1.55;color:var(--color-foreground)}.doc-token-bar{display:flex;align-items:center;justify-content:space-between;gap:.85rem;flex-wrap:wrap;padding:.65rem .85rem;background:linear-gradient(90deg,rgba(85,63,131,.1) 0%,rgba(85,63,131,.02) 70%,transparent 100%),var(--color-background);border:1px solid var(--color-border);border-radius:.6rem}.doc-token-btn{display:inline-flex;align-items:center;gap:.45rem;padding:.45rem .85rem;font-family:var(--font-sans);font-size:12.5px;font-weight:500;color:var(--color-foreground);background:var(--color-surface);border:1px solid var(--color-border);border-radius:.4rem;cursor:pointer;transition:background .12s,border-color .12s}.doc-token-btn:hover{background:var(--color-surface-hover);border-color:#553f8399}.doc-token-btn:disabled{opacity:.5;cursor:not-allowed}.doc-details{background:var(--color-background);border:1px solid var(--color-border);border-radius:.6rem;overflow:hidden}.doc-details-summary{list-style:none;display:flex;align-items:center;gap:.5rem;padding:.7rem .95rem;cursor:pointer;font-family:var(--font-sans);font-size:13px;font-weight:500;color:var(--color-foreground);-webkit-user-select:none;-moz-user-select:none;user-select:none;transition:background .12s}.doc-details-summary::-webkit-details-marker{display:none}.doc-details-summary:hover{background:#ffffff05}.doc-details[open]>.doc-details-summary{border-bottom:1px solid var(--color-border)}.doc-details-body{padding:.85rem}.codeblock{background:var(--color-background);border:1px solid var(--color-border);border-radius:.6rem;overflow:hidden}.codeblock-bar{display:flex;align-items:center;justify-content:space-between;padding:.45rem .85rem;background:var(--color-surface);border-bottom:1px solid var(--color-border)}.codeblock-lang{font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--color-foreground-muted)}.codeblock-copy{display:inline-flex;align-items:center;gap:.35rem;padding:.3rem .6rem;background:var(--color-background);border:1px solid var(--color-border);border-radius:.35rem;color:var(--color-foreground-muted);font-family:var(--font-sans);font-size:11.5px;cursor:pointer;transition:color .12s,border-color .12s,background .12s}.codeblock-copy:hover{color:var(--color-foreground);border-color:#553f8399;background:var(--color-surface-hover)}.codeblock-pre{margin:0;padding:.95rem 1.1rem;overflow-x:auto;font-family:var(--font-mono);font-size:12.5px;line-height:1.6;color:var(--color-foreground-muted);background:var(--color-background)}.codeblock-pre code{background:transparent!important;padding:0!important;font-family:inherit;font-size:inherit;line-height:inherit}.tabbed{background:var(--color-background);border:1px solid var(--color-border);border-radius:.6rem;overflow:hidden}.tabbed-tabs{display:flex;flex-wrap:wrap;gap:0;background:var(--color-surface);border-bottom:1px solid var(--color-border);padding:0 .35rem}.tabbed-tab{position:relative;background:transparent;border:0;padding:.6rem .95rem;font-family:var(--font-sans);font-size:12.5px;font-weight:500;color:var(--color-foreground-muted);cursor:pointer;transition:color .12s}.tabbed-tab:hover{color:var(--color-foreground)}.tabbed-tab.active{color:var(--color-foreground);font-weight:600}.tabbed-tab.active:after{content:"";position:absolute;left:.6rem;right:.6rem;bottom:-1px;height:2px;background:var(--color-primary);border-radius:2px 2px 0 0}.tabbed-note{padding:.65rem .95rem;border-bottom:1px solid var(--color-border);background:var(--color-surface);color:var(--color-foreground-muted);font-family:var(--font-sans);font-size:12px;line-height:1.55}.tabbed>.codeblock{border:0;border-radius:0}.callout{display:flex;flex-direction:column;gap:.4rem;padding:.85rem 1rem;background:linear-gradient(90deg,rgba(85,63,131,.08) 0%,rgba(85,63,131,.01) 60%,transparent 100%),var(--color-background);border:1px solid var(--color-border);border-radius:.6rem}.callout-head{display:flex;align-items:center;gap:.5rem;font-family:var(--font-mono);font-size:10px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--color-foreground-muted)}.callout-icon{width:.95rem;height:.95rem}.callout-body{display:flex;flex-wrap:wrap;gap:.3rem .4rem;align-items:center;font-family:var(--font-sans);font-size:13px;line-height:1.55;color:var(--color-foreground)}.ai-prompt-actions{display:flex;align-items:center;flex-wrap:wrap;gap:.75rem}.ai-copy-btn{display:inline-flex;align-items:center;gap:.4rem;padding:.4rem .75rem;border-radius:6px;border:1px solid var(--color-border);background:var(--color-surface);color:var(--color-foreground-muted);font-size:12px;cursor:pointer;transition:color .15s ease,border-color .15s ease}.ai-copy-btn:hover{color:#fff;border-color:var(--color-foreground-muted)}.ai-copy-btn.copied{color:var(--color-success-fg);border-color:#4ade8066}.codeblock-pre .hljs{background:transparent!important;color:inherit;padding:0} diff --git a/backend/internal/server/ui_dist/assets/Docs-uPgaD7zh.js b/backend/internal/server/ui_dist/assets/Docs-uPgaD7zh.js new file mode 100644 index 0000000..254137e --- /dev/null +++ b/backend/internal/server/ui_dist/assets/Docs-uPgaD7zh.js @@ -0,0 +1,327 @@ +import{E as W}from"./format-CsU4_SPu.js";import{c as Y}from"./clipboard-CmSw2rR-.js";import{a as Ee,b as De}from"./aiPrompts-Dgb3jxRL.js";import{G as Re,o as J,J as He,a as g,b as e,s as P,n as j,f as n,F as _,p as A,d,l as b,k as a,h as C,bb as $e,t as l,g as Me,r as k,q as v,aF as x,z as s,a5 as Le,D as qe,j as u,K as Z,a1 as Ne}from"./index-CmY58qNN.js";import{H as f,p as Be,j as Q,a as ze,b as N}from"./github-dark-BrynTfs3.js";import{C as B}from"./check-z8qyH5P-.js";import{C as z}from"./copy-BzujsGZw.js";import{G as K}from"./globe-D5WsrNJb.js";import{C as ee}from"./chevron-right-9PDhxIr8.js";import{K as M}from"./key-round-ByFVPOPP.js";import{C as Ke}from"./chevron-down-BMYhN6hn.js";import{V as Ue}from"./variable-CG75qovQ.js";import{L as Fe}from"./lock-DHycfcno.js";function Xe(L){const q=L.regex,r="HTTP/([32]|1\\.[01])",I=/[A-Za-z][A-Za-z0-9-]*/,E={className:"attribute",begin:q.concat("^",I,"(?=\\:\\s)"),starts:{contains:[{className:"punctuation",begin:/: /,relevance:0,starts:{end:"$",relevance:0}}]}},T=[E,{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0}}];return{name:"HTTP",aliases:["https"],illegal:/\S/,contains:[{begin:"^(?="+r+" \\d{3})",end:/$/,contains:[{className:"meta",begin:r},{className:"number",begin:"\\b\\d{3}\\b"}],starts:{end:/\b\B/,illegal:/\S/,contains:T}},{begin:"(?=^[A-Z]+ (.*?) "+r+"$)",end:/$/,contains:[{className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{className:"meta",begin:r},{className:"keyword",begin:"[A-Z]+"}],starts:{end:/\b\B/,illegal:/\S/,contains:T}},L.inherit(E,{relevance:0})]}}const Ve={class:"space-y-12 pb-16"},Ge={class:"docs-hero"},We={class:"docs-hero-content"},Ye={class:"docs-hero-row"},Je={class:"docs-hero-actions"},Ze=["title","aria-label"],Qe={class:"docs-hero-toc","aria-label":"Jump to docs section"},eo=["href"],oo={class:"docs-hero-toc-num"},so={id:"handler",class:"space-y-5 scroll-mt-6"},to={class:"doc-table-wrap"},ao={class:"doc-table"},no={class:"doc-cell-key"},co={class:"doc-cell-mono"},ro={class:"doc-cell-mono hidden sm:table-cell"},io={class:"doc-cell-mono hidden md:table-cell"},lo={id:"deploy",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},po={class:"grid grid-cols-1 lg:grid-cols-2 gap-3"},uo={class:"space-y-2"},ho={class:"space-y-2"},vo={class:"space-y-2"},mo={id:"config",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},bo={class:"doc-table-wrap"},go={class:"doc-table"},yo={class:"doc-cell-key whitespace-nowrap"},fo={class:"doc-cell-mono hidden sm:table-cell whitespace-nowrap"},ko={class:"doc-cell-body"},wo={class:"space-y-2"},Co={class:"doc-details group"},xo={class:"doc-details-summary"},To={class:"doc-details-body"},So={id:"sdk",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},_o={class:"space-y-2"},Ao={class:"space-y-2"},Oo={class:"space-y-2"},Po={id:"schedules",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},jo={class:"doc-section-head"},Io={class:"doc-lede"},Eo={id:"webhooks",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},Do={class:"doc-section-head"},Ro={class:"doc-lede"},Ho={class:"doc-table-wrap"},$o={class:"doc-table"},Mo={class:"doc-cell-key whitespace-nowrap"},Lo={class:"doc-cell-body"},qo={class:"space-y-2"},No={id:"mcp",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},Bo={class:"grid grid-cols-1 md:grid-cols-3 gap-3"},zo={class:"doc-card"},Ko={class:"doc-card-body"},Uo={class:"doc-chip break-all"},Fo={class:"doc-token-bar"},Xo={class:"flex items-center gap-2 min-w-0 flex-1"},Vo={key:0,class:"text-sm text-foreground-muted truncate"},Go={key:1,class:"text-sm text-success truncate"},Wo={class:"doc-chip"},Yo=["disabled"],Jo={class:"doc-details group"},Zo={class:"doc-details-summary"},Qo={class:"doc-details-body space-y-4"},es={id:"generate",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},os={class:"ai-prompt-actions"},ss={key:0,class:"prompt-collapse-fade","aria-hidden":"true"},ts=["aria-expanded"],as={id:"tracing",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},ns={class:"doc-table-wrap"},cs={class:"doc-table"},ds={class:"doc-cell-key whitespace-nowrap"},rs={class:"doc-cell-body"},is={id:"errors",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},ls={class:"doc-table-wrap"},ps={class:"doc-table"},us={class:"doc-cell-key whitespace-nowrap"},hs={class:"doc-cell-body"},vs={id:"cli",class:"space-y-5 scroll-mt-6 border-t border-border pt-12"},ms={class:"doc-prose"},bs={class:"doc-table-wrap"},gs={class:"doc-table"},ys={class:"doc-cell-key whitespace-nowrap"},fs={class:"doc-cell-mono"},ks={class:"doc-cell-body hidden md:table-cell"},ws={class:"space-y-2"},Cs={class:"space-y-2"},xs={class:"space-y-2"},Ts={class:"space-y-2"},Ss={class:"space-y-2"},_s=`# Available inside every running function — refresh per-invocation: +ORVA_TRACE_ID=tr_3e39f6991c66f140577c6021da7dd13b # one per causal chain +ORVA_SPAN_ID=sp_4ceba57f6b1c982e # this execution + +# Python: os.environ["ORVA_TRACE_ID"] +# Node.js: process.env.ORVA_TRACE_ID +# Reading them is optional — the platform records the trace for you.`,As=`// Function A — calls B via the SDK. Trace context flows automatically. +const { invoke, jobs } = require('orva') + +module.exports.handler = async (event) => { + // F2F call — B becomes a child span under A. + const result = await invoke('send_email', { to: event.email }) + + // Job enqueue — when this job runs (now or in 6 hours), the resulting + // execution lands in the SAME trace as A. + await jobs.enqueue('audit_log', { action: 'sent', to: event.email }) + + return { statusCode: 200, body: 'ok' } +}`,Os=`# Send the W3C traceparent header — Orva will adopt it as the trace root. +curl -H "traceparent: 00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-01" \\ + https://orva.example.com/fn/myfn/ + +# Response always echoes: +# X-Trace-Id: tr_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`,Ps=`{ + "error": { + "code": "VALIDATION", + "message": "name must be lowercase and dash-separated", + "request_id": "req_abc123" + } +}`,js=`# 1. Generate an API key in the dashboard (Keys page) or via the API +# 2. Tell the CLI where to find your Orva and which key to use +orva login \\ + --endpoint https://orva.example.com \\ + --api-key orva_xxx_your_key_here + +# Writes ~/.orva/config.yaml. Subsequent commands need no flags. +orva system health # smoke test`,Is=`# Init a project in cwd (creates orva.yaml + handler stub) +orva init + +# Deploy from a directory. Auto-detects handler.ts when tsconfig.json +# is present; else uses the runtime default (handler.js / handler.py). +orva deploy ./my-fn \\ + --name resize-image \\ + --runtime node24 + +# Override the entrypoint explicitly: +orva deploy ./my-fn --name api --runtime python314 --entrypoint app.py`,Es=`# Invoke a function by name or fn_: +orva invoke resize-image --data '{"url":"https://example.com/cat.jpg"}' + +# Recent executions: +orva logs resize-image + +# Single execution, with stdout/stderr: +orva logs resize-image --exec-id exec_abc123 + +# Live tail — SSE stream, Ctrl-C to stop: +orva logs resize-image --tail`,Ds=`# List keys (optionally by prefix) +orva kv list resize-image +orva kv list resize-image --prefix user: + +# Read / write / delete +orva kv get resize-image cache:home +orva kv put resize-image cache:home '{"hits":42}' --ttl 3600 +orva kv delete resize-image cache:home`,Rs=`# Secrets — encrypted at rest, injected as env vars at spawn: +orva secrets set resize-image S3_BUCKET my-bucket +orva secrets list resize-image +orva secrets delete resize-image S3_BUCKET + +# Cron — fire a function on a schedule: +orva cron create --fn daily-report --expr '0 9 * * *' --tz Asia/Kolkata +orva cron list +orva cron update --enabled false # pause +orva cron delete + +# Jobs — fire-and-forget background queue: +orva jobs enqueue --fn send-email --data '{"to":"a@b.c"}' +orva jobs list --status pending +orva jobs retry +orva jobs delete + +# Outbound webhooks (system events): +orva webhooks create --url https://hooks.slack.com/... --events deployment.failed,job.failed +orva webhooks test + +# Inbound webhook triggers (external POST → function): +orva webhooks inbound create --fn order-handler --signature stripe`,Hs=`orva system health # daemon up + DB ok +orva system metrics # JSON metrics snapshot +orva system db-stats # on-disk breakdown (orva.db, WAL, functions/) +orva system vacuum # rewrite SQLite to reclaim freelist pages + +orva activity # last 50 activity rows +orva activity --tail # live feed (Ctrl-C) +orva activity --source mcp --limit 200 # MCP-only, last 200`,oe="",Ys={__name:"Docs",setup(L){const q=Re();f.registerLanguage("python",Be),f.registerLanguage("javascript",Q),f.registerLanguage("js",Q),f.registerLanguage("json",ze),f.registerLanguage("bash",N),f.registerLanguage("shell",N),f.registerLanguage("sh",N),f.registerLanguage("http",Xe);const r=v(()=>window.location.origin),I=[{id:"handler",num:"01",label:"Handler"},{id:"deploy",num:"02",label:"Deploy"},{id:"config",num:"03",label:"Config"},{id:"sdk",num:"04",label:"SDK"},{id:"schedules",num:"05",label:"Schedules"},{id:"webhooks",num:"06",label:"Webhooks"},{id:"mcp",num:"07",label:"MCP"},{id:"generate",num:"08",label:"AI prompt"},{id:"tracing",num:"09",label:"Tracing"},{id:"errors",num:"10",label:"Errors"},{id:"cli",num:"11",label:"CLI"}],E=k("handler");let T=null;J(()=>{if(typeof IntersectionObserver>"u")return;const c=new Set;T=new IntersectionObserver(o=>{for(const i of o)i.isIntersecting?c.add(i.target.id):c.delete(i.target.id);for(const i of I)if(c.has(i.id)){E.value=i.id;break}},{rootMargin:"-20% 0px -70% 0px",threshold:0});for(const o of I){const i=document.getElementById(o.id);i&&T.observe(i)}}),He(()=>{T&&T.disconnect()});const se=De(),D=k(!1);let U=null;const te=async()=>{await Ee()&&(D.value=!0,clearTimeout(U),U=setTimeout(()=>{D.value=!1},1500))},F=x({setup(){return()=>s("svg",{viewBox:"0 0 256 255",width:"14",height:"14",xmlns:"http://www.w3.org/2000/svg"},[s("defs",null,[s("linearGradient",{id:"pyg1",x1:"0",y1:"0",x2:"1",y2:"1"},[s("stop",{offset:"0","stop-color":"#387EB8"}),s("stop",{offset:"1","stop-color":"#366994"})]),s("linearGradient",{id:"pyg2",x1:"0",y1:"0",x2:"1",y2:"1"},[s("stop",{offset:"0","stop-color":"#FFE052"}),s("stop",{offset:"1","stop-color":"#FFC331"})])]),s("path",{fill:"url(#pyg1)",d:"M126.9 12c-58.3 0-54.7 25.3-54.7 25.3l.1 26.2H128v8H50.5S12 67.2 12 126.1c0 58.9 33.6 56.8 33.6 56.8h19.4v-27.4s-1-33.6 33.1-33.6h55.9s32 .5 32-30.9V43.5S191.7 12 126.9 12zM95.7 29.9a10 10 0 0 1 0 20 10 10 0 0 1 0-20z"}),s("path",{fill:"url(#pyg2)",d:"M129.1 243c58.3 0 54.7-25.3 54.7-25.3l-.1-26.2H128v-8h77.5s38.5 4.4 38.5-54.5c0-58.9-33.6-56.8-33.6-56.8h-19.4v27.4s1 33.6-33.1 33.6H102s-32-.5-32 30.9v52S64.3 243 129.1 243zm30.4-17.9a10 10 0 0 1 0-20 10 10 0 0 1 0 20z"})])}}),X=x({setup(){return()=>s("svg",{viewBox:"0 0 256 280",width:"14",height:"14",xmlns:"http://www.w3.org/2000/svg"},[s("path",{fill:"#3F873F",d:"M128 0 12 67v146l116 67 116-67V67L128 0zm0 24.6 95 54.8v121.2l-95 54.8-95-54.8V79.4l95-54.8z"}),s("path",{fill:"#3F873F",d:"M128 64c-3 0-5.7.7-8 2.3L73 92c-5 2.7-8 8-8 13.6V169c0 5.6 3 10.7 8 13.5l13 7.4c6.3 3.1 8.5 3.1 11.4 3.1 9.4 0 14.8-5.7 14.8-15.6V117c0-1-.7-1.7-1.7-1.7H103c-1 0-1.7.7-1.7 1.7v60.2c0 4.4-4.5 8.7-11.8 5.1l-13.7-7.9a1.6 1.6 0 0 1-.8-1.4v-63.4c0-.6.3-1 .8-1.4l46.8-26.9c.4-.3 1-.3 1.4 0L171 110c.5.4.8.8.8 1.4V174a1.7 1.7 0 0 1-.8 1.4l-46.8 27c-.4.2-1 .2-1.4 0l-12-7.2c-.4-.2-.8-.2-1.2 0-3.4 1.9-4 2.2-7.2 3.3-.8.3-2 .7.4 2.1l15.7 9.3c2.5 1.4 5.3 2.2 8.2 2.2 2.9 0 5.7-.8 8.2-2.2L181 184c5-2.8 8-7.9 8-13.5V107c0-5.6-3-10.7-8-13.5l-46.7-26.7a17 17 0 0 0-6.3-2.8z"})])}}),ae=x({name:"DeployPipelineDiagram",setup(){const c=[{glyph:"▣",label:"Tarball",sub:"POST /deploy"},{glyph:"⟜",label:"Extract",sub:"untar → scratch dir"},{glyph:"◍",label:"Install",sub:"npm / pip"},{glyph:"⟐",label:"Compile",sub:"tsc (TypeScript)"},{glyph:"◉",label:"Activate",sub:"rename → current"},{glyph:"✦",label:"Warm pool",sub:"pre-spawn N workers"}];return()=>s("figure",{class:"doc-diagram"},[s("figcaption",{class:"doc-diagram-cap"},"Deploy pipeline"),s("div",{class:"doc-pipeline"},c.flatMap((o,i)=>{const t=s("div",{key:`s${i}`,class:"doc-pipeline-stage"},[s("div",{class:"doc-pipeline-glyph"},o.glyph),s("div",{class:"doc-pipeline-label"},[s("span",{class:"doc-pipeline-name"},o.label),s("span",{class:"doc-pipeline-sub"},o.sub)])]),p=it/220*100;return()=>s("figure",{class:"doc-diagram"},[s("figcaption",{class:"doc-diagram-cap"},"Causal trace, one HTTP request and three spans"),s("div",{class:"doc-trace"},[s("div",{class:"doc-trace-axis"},[s("span",null,"0 ms"),s("span",null,"220 ms")]),...o.map(t=>s("div",{key:t.fn,class:["doc-trace-row",`is-${t.klass}`]},[s("div",{class:"doc-trace-label"},[s("span",{class:"doc-trace-fn"},t.fn),s("span",{class:"doc-trace-trigger"},t.trigger)]),s("div",{class:"doc-trace-track"},[s("div",{class:"doc-trace-bar",style:{left:`${i(t.start)}%`,width:`${i(t.dur)}%`},title:`+${t.start}ms · ${t.dur}ms`})]),s("div",{class:"doc-trace-dur"},`${t.dur}ms`)])),s("div",{class:"doc-trace-legend"},[s("span",null,"Same "),s("code",{class:"doc-chip"},"trace_id"),s("span",null," across all spans · "),s("code",{class:"doc-chip"},"parent_span_id"),s("span",null," chains them into a tree.")])])])}}),ce=x({name:"WebhookDeliveryDiagram",setup(){return()=>s("figure",{class:"doc-diagram"},[s("figcaption",{class:"doc-diagram-cap"},"Signed webhook delivery"),s("div",{class:"doc-webhook"},[s("div",{class:"doc-webhook-actor"},[s("div",{class:"doc-webhook-actor-head"},"orvad"),s("div",{class:"doc-webhook-actor-body"},[s("span",null,"event fires"),s("code",{class:"doc-chip"},"deployment.succeeded")])]),s("div",{class:"doc-webhook-wire"},[s("div",{class:"doc-webhook-wire-line","aria-hidden":"true"}),s("div",{class:"doc-webhook-wire-payload"},[s("div",{class:"doc-webhook-wire-method"},"POST"),s("div",{class:"doc-webhook-wire-headers"},[s("code",null,"X-Orva-Event"),s("code",null,"X-Orva-Timestamp"),s("code",null,"X-Orva-Signature")]),s("div",{class:"doc-webhook-wire-sig"},"sha256=hex(hmac(secret, ts.body))")])]),s("div",{class:"doc-webhook-actor"},[s("div",{class:"doc-webhook-actor-head"},"your receiver"),s("div",{class:"doc-webhook-actor-body"},[s("span",null,"verify HMAC"),s("span",null,"→ 2xx within 15s or get retried")])])])])}}),de=v(()=>[{label:"Python",lang:"python",code:`def handler(event): + body = event.get("body") or {} + return { + "statusCode": 200, + "headers": {"Content-Type": "application/json"}, + "body": {"hello": body.get("name", "world")}, + }`},{label:"Node.js",lang:"js",code:`exports.handler = async (event) => { + const body = event.body || {}; + return { + statusCode: 200, + headers: { 'Content-Type': 'application/json' }, + body: { hello: body.name || 'world' }, + }; +};`}]),re=v(()=>[{label:"curl",lang:"bash",code:`curl -X POST ${r.value}/fn/ \\ + -H 'Content-Type: application/json' \\ + -d '{"name": "Orva"}'`},{label:"fetch",lang:"js",code:`const res = await fetch('${r.value}/fn/', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: 'Orva' }), +}); +console.log(await res.json());`},{label:"Python",lang:"python",code:`import httpx + +r = httpx.post( + "${r.value}/fn/", + json={"name": "Orva"}, +) +print(r.json())`}]),ie=[{id:"python314",name:"Python 3.14",entry:"handler.py",deps:"requirements.txt",icon:F},{id:"python313",name:"Python 3.13",entry:"handler.py",deps:"requirements.txt",icon:F},{id:"node24",name:"Node.js 24",entry:"handler.js",deps:"package.json",icon:X},{id:"node22",name:"Node.js 22",entry:"handler.js",deps:"package.json",icon:X}],le=[{field:"env_vars",purpose:"Plain config",body:"Plaintext config stored on the function record. Use for feature flags and non-secret settings.",icon:Ue,iconClass:"text-violet-300"},{field:"/secrets",purpose:"Encrypted",body:"AES-256-GCM at rest. Values decrypt only into the worker environment at spawn time.",icon:M,iconClass:"text-emerald-300"},{field:"network_mode",purpose:"Egress control",body:"none = isolated loopback. egress = outbound HTTPS allowed; firewall blocklist applies.",icon:K,iconClass:"text-sky-300"},{field:"auth_mode",purpose:"Invoke gate",body:"none = public. platform_key = require Orva API key. signed = require HMAC.",icon:Fe,iconClass:"text-violet-300"},{field:"rate_limit_per_min",purpose:"Per-IP throttle",body:"Optional cap for public or webhook-facing functions. Exceeding it returns 429.",icon:Ne,iconClass:"text-amber-300"}],pe=v(()=>`curl -X POST ${r.value}/api/v1/functions \\ + -H 'X-Orva-API-Key: ' \\ + -H 'Content-Type: application/json' \\ + -d '{"name":"hello","runtime":"python314","memory_mb":128,"cpus":0.5}'`),ue=v(()=>`tar czf code.tar.gz handler.py requirements.txt +curl -X POST ${r.value}/api/v1/functions//deploy \\ + -H 'X-Orva-API-Key: ' \\ + -F code=@code.tar.gz`),he=v(()=>`curl -X POST ${r.value}/api/v1/functions//secrets \\ + -H 'X-Orva-API-Key: ' \\ + -H 'Content-Type: application/json' \\ + -d '{"key":"DATABASE_URL","value":"postgres://..."}'`),ve=v(()=>`# generate signature +SECRET='your-shared-secret-stored-in-function-secrets' +TS=$(date +%s) +BODY='{"hello":"world"}' +SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $2}') + +curl -X POST ${r.value}/fn/ \\ + -H "X-Orva-Timestamp: $TS" \\ + -H "X-Orva-Signature: sha256=$SIG" \\ + -H 'Content-Type: application/json' \\ + -d "$BODY"`),me=v(()=>[{label:"curl",lang:"bash",note:"Create a daily-9am schedule for an existing function. payload is delivered as the invoke body.",code:`curl -X POST ${r.value}/api/v1/functions//cron \\ + -H 'X-Orva-API-Key: ' \\ + -H 'Content-Type: application/json' \\ + -d '{ + "cron_expr": "0 9 * * *", + "enabled": true, + "payload": {"task": "daily-summary"} + }'`},{label:"Toggle / edit",lang:"bash",note:"PUT accepts any subset of {cron_expr, enabled, payload}; omitted fields keep their previous value. next_run_at is recomputed on expr changes.",code:`# pause +curl -X PUT ${r.value}/api/v1/functions//cron/ \\ + -H 'X-Orva-API-Key: ' \\ + -H 'Content-Type: application/json' \\ + -d '{"enabled": false}' + +# change schedule +curl -X PUT ${r.value}/api/v1/functions//cron/ \\ + -H 'X-Orva-API-Key: ' \\ + -H 'Content-Type: application/json' \\ + -d '{"cron_expr": "*/15 * * * *"}'`},{label:"List & delete",lang:"bash",note:"GET /api/v1/cron lists every schedule across functions (with function_name JOIN); per-function uses the nested route.",code:`# all schedules +curl ${r.value}/api/v1/cron \\ + -H 'X-Orva-API-Key: ' + +# delete one +curl -X DELETE ${r.value}/api/v1/functions//cron/ \\ + -H 'X-Orva-API-Key: '`}]),be=[{label:"Python",lang:"python",code:`from orva import kv + +def handler(event): + # Store with optional TTL (seconds). 0 = no expiry. + kv.put("user:42", {"name": "Ada", "tier": "pro"}, ttl_seconds=3600) + + # Read; default returned if missing or expired. + user = kv.get("user:42", default=None) + + # List by prefix. + pages = kv.list(prefix="page:", limit=50) + + # Delete is idempotent. + kv.delete("user:42") + + return {"statusCode": 200, "body": str(user)}`},{label:"Node.js",lang:"js",code:`const { kv } = require('orva') + +exports.handler = async (event) => { + await kv.put('user:42', { name: 'Ada', tier: 'pro' }, { ttlSeconds: 3600 }) + + const user = await kv.get('user:42', null) + + const pages = await kv.list({ prefix: 'page:', limit: 50 }) + + await kv.delete('user:42') + + return { statusCode: 200, body: JSON.stringify(user) } +}`}],ge=[{label:"Python",lang:"python",code:`from orva import invoke, OrvaError + +def handler(event): + try: + # invoke() returns the downstream {statusCode, headers, body}. + # body is JSON-decoded when possible. + result = invoke("resize-image", {"url": event["body"]["url"]}) + return {"statusCode": 200, "body": result["body"]} + except OrvaError as e: + # 404 = function not found, 507 = call depth exceeded. + return {"statusCode": e.status or 502, "body": str(e)}`},{label:"Node.js",lang:"js",code:`const { invoke, OrvaError } = require('orva') + +exports.handler = async (event) => { + try { + const result = await invoke('resize-image', { url: event.body.url }) + return { statusCode: 200, body: result.body } + } catch (e) { + if (e instanceof OrvaError) { + return { statusCode: e.status || 502, body: e.message } + } + throw e + } +}`}],ye=[{label:"Python",lang:"python",code:`from orva import jobs + +def handler(event): + # Fire-and-forget. Returns the job id immediately; the function + # body runs later via the scheduler. max_attempts retries with + # exponential backoff on 5xx / exception. + job_id = jobs.enqueue( + "send-welcome-email", + {"to": event["body"]["email"]}, + max_attempts=3, + ) + return {"statusCode": 202, "body": job_id}`},{label:"Node.js",lang:"js",code:`const { jobs } = require('orva') + +exports.handler = async (event) => { + const jobId = await jobs.enqueue( + 'send-welcome-email', + { to: event.body.email }, + { maxAttempts: 3 } + ) + return { statusCode: 202, body: jobId } +}`}],fe=[{name:"deployment.succeeded",when:"A function build finished and the new version is active."},{name:"deployment.failed",when:"A build failed or was rejected."},{name:"function.created",when:"A new function row was created via POST /api/v1/functions."},{name:"function.updated",when:"A function config was edited via PUT /api/v1/functions/{id} (status flips during a deploy do NOT fire this; see deployment.*)."},{name:"function.deleted",when:"A function was removed."},{name:"execution.error",when:"An invocation finished with status=error or 5xx."},{name:"cron.failed",when:"A scheduled run failed (bad expr, missing fn, dispatch error, or 5xx)."},{name:"job.succeeded",when:"A queued background job finished successfully."},{name:"job.failed",when:"A queued job exhausted its retries (terminal failure)."}],ke=[{label:"Python",lang:"python",note:"Run on the receiver. Reject anything that fails verification. The signature ensures the request really came from this Orva instance.",code:`import hmac, hashlib, time + +def verify(secret: str, ts: str, body: bytes, sig_header: str) -> bool: + if abs(time.time() - int(ts)) > 300: # 5-min skew window + return False + mac = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256) + expected = "sha256=" + mac.hexdigest() + return hmac.compare_digest(expected, sig_header) + +# In your Flask/FastAPI/etc. handler: +ts = request.headers["X-Orva-Timestamp"] +sig = request.headers["X-Orva-Signature"] +if not verify(WEBHOOK_SECRET, ts, request.get_data(), sig): + return "bad signature", 401`},{label:"Node.js",lang:"js",note:"Same shape as Stripe. Use timingSafeEqual to avoid sig-leak via timing.",code:`const crypto = require('crypto') + +function verify(secret, ts, body, sigHeader) { + if (Math.abs(Date.now() / 1000 - parseInt(ts, 10)) > 300) return false + const mac = crypto.createHmac('sha256', secret) + mac.update(ts + '.') + mac.update(body) + const expected = 'sha256=' + mac.digest('hex') + if (expected.length !== sigHeader.length) return false + return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sigHeader)) +} + +// In an express handler with raw body middleware: +app.post('/webhooks/orva', (req, res) => { + const ok = verify( + process.env.WEBHOOK_SECRET, + req.headers['x-orva-timestamp'], + req.body, // raw bytes — NOT parsed JSON + req.headers['x-orva-signature'] + ) + if (!ok) return res.status(401).send('bad signature') + res.sendStatus(200) +})`}],we=[{name:"http",desc:"Public HTTP request hit /fn//. Almost always a root span."},{name:"f2f",desc:"Another function called this one via orva.invoke(). Has a parent_span_id."},{name:"job",desc:"Background job runner picked up an enqueued job. Parent_span_id is whoever enqueued it."},{name:"cron",desc:"Scheduler fired a cron entry. Always a root span."},{name:"inbound",desc:"External webhook hit /webhook/{id}. Always a root span."},{name:"replay",desc:"Operator clicked Replay on a captured execution. Fresh trace, no link to original."},{name:"mcp",desc:"AI agent invoked the function via MCP invoke_function. Fresh trace."}],Ce=[{code:"VALIDATION",when:"Bad request body or path parameter."},{code:"UNAUTHORIZED",when:"Missing or invalid API key / session cookie."},{code:"NOT_FOUND",when:"Function, deployment, or secret doesn't exist."},{code:"RATE_LIMITED",when:"Too many requests; check the Retry-After header."},{code:"VERSION_GCD",when:"Rollback target was garbage-collected."},{code:"INSUFFICIENT_DISK",when:"Host is below min_free_disk_mb."}],xe=[{cmd:"login",subs:W,purpose:"Save endpoint + API key to ~/.orva/config.yaml"},{cmd:"init",subs:W,purpose:"Scaffold an orva.yaml in the current directory"},{cmd:"deploy",subs:"[path]",purpose:"Package a directory and deploy as a function"},{cmd:"invoke",subs:"[name|id]",purpose:"POST to /fn// and print the response"},{cmd:"logs",subs:"[name|id] [--tail]",purpose:"List recent executions; --tail follows live via SSE"},{cmd:"functions",subs:"list / get / create / delete",purpose:"CRUD for the function registry"},{cmd:"cron",subs:"list / create / update / delete",purpose:"Manage cron schedules attached to functions"},{cmd:"jobs",subs:"list / enqueue / retry / delete",purpose:"Background queue management"},{cmd:"kv",subs:"list / get / put / delete",purpose:"Browse a function’s key/value store"},{cmd:"secrets",subs:"list / set / delete",purpose:"AES-256-GCM secrets per function"},{cmd:"webhooks",subs:"list / create / test / delete / inbound",purpose:"System-event subscribers + inbound triggers"},{cmd:"routes",subs:"list / set / delete",purpose:"Custom URL → function path mappings"},{cmd:"keys",subs:"list / create / revoke",purpose:"Manage API keys"},{cmd:"activity",subs:"[--tail] [--source web|api|...]",purpose:"Paginated activity rows; live SSE with --tail"},{cmd:"system",subs:"health / metrics / db-stats / vacuum",purpose:"Server diagnostics"},{cmd:"setup",subs:"[--skip-nsjail] [--skip-rootfs]",purpose:"Install nsjail + rootfs on a bare host"},{cmd:"serve",subs:"[--port N]",purpose:"Run as the server daemon (not the CLI client)"},{cmd:"completion",subs:"bash / zsh / fish / powershell",purpose:"Emit shell completion script"}],V=k("");J(async()=>{try{const c=await fetch("/web/docs.md",{cache:"no-cache"});c.ok&&(V.value=await c.text())}catch{}});const Te=v(()=>V.value.replaceAll("{{ORIGIN}}",window.location.origin)),O=k(!1);let G=null;const Se=async()=>{await Y(Te.value)&&(O.value=!0,clearTimeout(G),G=setTimeout(()=>{O.value=!1},1500))},S=k(!1),R=k(""),H=k(!1),_e=v(()=>R.value.slice(0,12)),m=v(()=>R.value||oe),Ae=async()=>{if(!H.value){H.value=!0;try{const c=new Date().toISOString().slice(0,16).replace("T"," "),o=await qe.post("/keys",{name:"MCP: "+c,permissions:["invoke","read","write","admin"]});R.value=o.data.key}catch(c){console.error("mint mcp key failed",c),q.notify({title:"Could not mint key",message:c?.response?.data?.error?.message||c.message||"Unknown error",danger:!0})}finally{H.value=!1}}},Oe=v(()=>[{label:"Claude Code",lang:"bash",note:"Anthropic's `claude` CLI. Restart Claude Code afterwards; `/mcp` lists Orva's 70 tools.",code:`claude mcp add --transport http --scope user orva ${r.value}/mcp --header "Authorization: Bearer ${m.value}"`},{label:"curl",lang:"bash",note:"Talk to MCP directly. Step 1 returns a session id (Mcp-Session-Id) that Step 2 references.",code:`curl -sD - -X POST ${r.value}/mcp \\ + -H 'Authorization: Bearer ${m.value}' \\ + -H 'Content-Type: application/json' \\ + -H 'Accept: application/json, text/event-stream' \\ + -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}' + +curl -sX POST ${r.value}/mcp \\ + -H 'Authorization: Bearer ${m.value}' \\ + -H 'Content-Type: application/json' \\ + -H 'Accept: application/json, text/event-stream' \\ + -H 'Mcp-Session-Id: ' \\ + -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'`}]),Pe=v(()=>[{label:"Claude Desktop",lang:"json",note:"Paste into ~/Library/Application Support/Claude/claude_desktop_config.json (macOS), %APPDATA%\\Claude\\claude_desktop_config.json (Windows), or ~/.config/Claude/claude_desktop_config.json (Linux). Restart Claude Desktop.",code:`{ + "mcpServers": { + "orva": { + "url": "${r.value}/mcp", + "headers": { + "Authorization": "Bearer ${m.value}" + } + } + } +}`},{label:"Cursor",lang:"bash",note:"Open the link in your browser. Cursor pops an approval dialog and writes ~/.cursor/mcp.json.",code:`cursor://anysphere.cursor-deeplink/mcp/install?name=orva&config=${je.value}`},{label:"VS Code",lang:"bash",note:'User-scoped install via the Copilot-MCP `code --add-mcp` flag. Pick "Workspace" at the prompt to write .vscode/mcp.json instead.',code:`code --add-mcp '{"name":"orva","type":"http","url":"${r.value}/mcp","headers":{"Authorization":"Bearer ${m.value}"}}'`},{label:"Codex CLI",lang:"bash",note:"OpenAI's `codex` CLI. Writes to ~/.codex/config.toml.",code:`codex mcp add --transport streamable-http orva ${r.value}/mcp --header "Authorization: Bearer ${m.value}"`},{label:"OpenCode",lang:"bash",note:`Interactive add. Pick "Remote", paste ${r.value}/mcp, then add the header Authorization: Bearer ${m.value}.`,code:"opencode mcp add"},{label:"Zed",lang:"json",note:"Zed runs MCP as stdio subprocesses, so use the `mcp-remote` bridge. Paste under context_servers in ~/.config/zed/settings.json. Restart Zed.",code:`{ + "context_servers": { + "orva": { + "source": "custom", + "command": "npx", + "args": [ + "-y", "mcp-remote", + "${r.value}/mcp", + "--header", "Authorization:Bearer ${m.value}" + ] + } + } +}`},{label:"Windsurf",lang:"json",note:"Paste into ~/.codeium/windsurf/mcp_config.json and reload Windsurf.",code:`{ + "mcpServers": { + "orva": { + "serverUrl": "${r.value}/mcp", + "headers": { + "Authorization": "Bearer ${m.value}" + } + } + } +}`},{label:"claude.ai web",lang:"text",note:"UI-only flow. Settings → Connectors → Add custom connector. claude.ai opens an Orva login + consent popup and issues an OAuth 2.1 token automatically; no token paste required.",code:`URL: ${r.value}/mcp +Auth: OAuth (auto-discovered)`},{label:"ChatGPT",lang:"text",note:"UI-only flow. Settings → Apps & Connectors → Developer mode → Add new connector. ChatGPT discovers OIDC metadata, performs Dynamic Client Registration, and pops the Orva consent screen. No token paste required.",code:`URL: ${r.value}/mcp +Auth: OAuth (auto-discovered)`}]),je=v(()=>{const c=JSON.stringify({url:r.value+"/mcp",headers:{Authorization:"Bearer "+m.value}});return typeof window.btoa=="function"?window.btoa(c):c}),Ie=v(()=>[{label:"Cursor (global)",lang:"json",note:"Paste into ~/.cursor/mcp.json, or .cursor/mcp.json in your project root for a per-workspace install.",code:`{ + "mcpServers": { + "orva": { + "url": "${r.value}/mcp", + "headers": { + "Authorization": "Bearer ${m.value}" + } + } + } +}`},{label:"Cline",lang:"json",note:"In VS Code: open Cline → MCP icon → Configure MCP Servers. Cline writes cline_mcp_settings.json.",code:`{ + "mcpServers": { + "orva": { + "url": "${r.value}/mcp", + "headers": { + "Authorization": "Bearer ${m.value}" + }, + "disabled": false + } + } +}`}]),h=x({name:"CodeBlock",props:{code:{type:String,required:!0},lang:{type:String,default:""}},setup(c){const o=k(!1),i=async()=>{await Y(c.code)&&(o.value=!0,setTimeout(()=>{o.value=!1},1200))},t=v(()=>{const p=(c.lang||"").toLowerCase();if(p&&f.getLanguage(p))try{return f.highlight(c.code,{language:p,ignoreIllegals:!0}).value}catch{}return c.code.replace(/&/g,"&").replace(//g,">")});return()=>s("div",{class:"codeblock"},[s("div",{class:"codeblock-bar"},[s("span",{class:"codeblock-lang"},c.lang||""),s("button",{class:"codeblock-copy",onClick:i,title:"Copy code"},[o.value?s(B,{class:"w-3 h-3"}):s(z,{class:"w-3 h-3"}),o.value?"Copied":"Copy"])]),s("pre",{class:"codeblock-pre"},[s("code",{class:`hljs language-${(c.lang||"text").toLowerCase()}`,innerHTML:t.value})])])}}),y=x({name:"TabbedCode",props:{tabs:{type:Array,required:!0},storageKey:{type:String,default:""}},setup(c){const o=(()=>{try{if(c.storageKey){const p=localStorage.getItem(c.storageKey);if(p&&c.tabs.some(w=>w.label===p))return p}}catch{}return c.tabs[0]?.label})(),i=k(o),t=p=>{i.value=p;try{c.storageKey&&localStorage.setItem(c.storageKey,p)}catch{}};return()=>{const p=c.tabs.find(w=>w.label===i.value)||c.tabs[0];return s("div",{class:"tabbed"},[s("div",{class:"tabbed-tabs"},c.tabs.map(w=>s("button",{key:w.label,class:["tabbed-tab",{active:w.label===i.value}],onClick:()=>t(w.label)},w.label))),p.note?s("div",{class:"tabbed-note"},p.note):null,s(h,{code:p.code,lang:p.lang})])}}}),$=x({name:"Callout",props:{title:{type:String,default:""},icon:{type:[Object,Function],default:null}},setup(c,{slots:o}){return()=>s("div",{class:"callout"},[s("div",{class:"callout-head"},[c.icon?s(c.icon,{class:"callout-icon"}):null,c.title?s("span",null,c.title):null]),s("div",{class:"callout-body"},o.default?.())])}});return(c,o)=>{const i=Le("router-link");return u(),g("div",Ve,[e("header",Ge,[o[3]||(o[3]=e("div",{class:"docs-hero-bg","aria-hidden":"true"},null,-1)),e("div",We,[e("div",Ye,[o[1]||(o[1]=e("div",{class:"docs-hero-text"},[e("h1",{class:"docs-hero-title"}," Documentation "),e("p",{class:"docs-hero-sub"}," Everything you need to write, deploy, and operate functions on Orva. Handler contract, deploy + invoke, SDK, MCP, tracing, error taxonomy. ")],-1)),e("div",Je,[e("button",{class:P(["docs-hero-copy-icon",{copied:O.value}]),title:O.value?"Copied":"Copy entire docs page as Markdown","aria-label":O.value?"Markdown copied to clipboard":"Copy entire docs page as Markdown",onClick:Se},[O.value?(u(),j(n(B),{key:0,class:"w-4 h-4"})):(u(),j(n(z),{key:1,class:"w-4 h-4"}))],10,Ze)])]),e("nav",Qe,[o[2]||(o[2]=e("span",{class:"docs-hero-toc-label"},"Jump to",-1)),(u(),g(_,null,A(I,t=>e("a",{key:t.id,href:`#${t.id}`,class:P(["docs-hero-toc-link",{active:E.value===t.id}])},[e("span",oo,l(t.num),1),e("span",null,l(t.label),1)],10,eo)),64))])])]),e("section",so,[o[5]||(o[5]=e("div",{class:"doc-section-head"},[e("span",{class:"doc-section-num"},"01"),e("div",null,[e("h2",{class:"doc-section-title"}," Handler contract "),e("p",{class:"doc-lede"}," One exported function receives the inbound HTTP event and returns an HTTP-shaped response. The adapter handles serialization and headers. ")])],-1)),d(n(y),{tabs:de.value,"storage-key":"docs.handler"},null,8,["tabs"]),o[6]||(o[6]=b('
Event shape
methodpathheadersquerybody
Response
{ statusCode, headers, body }

Non-string bodies are JSON-encoded by the adapter.

Runtime env
Env vars and secrets land in process.env / os.environ.
',1)),e("div",to,[e("table",ao,[o[4]||(o[4]=e("thead",null,[e("tr",null,[e("th",null,"Runtime"),e("th",null,"ID"),e("th",{class:"hidden sm:table-cell"}," Entrypoint "),e("th",{class:"hidden md:table-cell"}," Dependencies ")])],-1)),e("tbody",null,[(u(),g(_,null,A(ie,t=>e("tr",{key:t.id},[e("td",no,[(u(),j(Z(t.icon),{class:"shrink-0"})),a(" "+l(t.name),1)]),e("td",co,l(t.id),1),e("td",ro,l(t.entry),1),e("td",io,l(t.deps),1)])),64))])])])]),e("section",lo,[o[11]||(o[11]=b('
02

Deploy & invoke

The dashboard handles day-to-day work; these calls are for CI and automation. Builds run async; poll /api/v1/deployments/<id> or stream /api/v1/deployments/<id>/stream until phase: done.

',1)),d(n(ae)),e("div",po,[e("div",uo,[o[7]||(o[7]=e("div",{class:"doc-step-label"},[e("span",{class:"doc-step-num"},"1"),a(" Create the function row ")],-1)),d(n(h),{code:pe.value,lang:"bash"},null,8,["code"])]),e("div",ho,[o[8]||(o[8]=e("div",{class:"doc-step-label"},[e("span",{class:"doc-step-num"},"2"),a(" Upload code ")],-1)),d(n(h),{code:ue.value,lang:"bash"},null,8,["code"])])]),e("div",vo,[o[9]||(o[9]=e("div",{class:"doc-microlabel"}," Invoke ",-1)),d(n(y),{tabs:re.value,"storage-key":"docs.invoke"},null,8,["tabs"])]),d(n($),{icon:n(K),title:"Custom routes"},{default:C(()=>[...o[10]||(o[10]=[a(" Attach a friendly path with ",-1),e("code",{class:"doc-chip"},"POST /api/v1/routes",-1),a(". Reserved prefixes: ",-1),e("code",{class:"doc-chip"},"/api/",-1),e("code",{class:"doc-chip"},"/fn/",-1),e("code",{class:"doc-chip"},"/mcp/",-1),e("code",{class:"doc-chip"},"/web/",-1),e("code",{class:"doc-chip"},"/_orva/",-1),a(". ",-1)])]),_:1},8,["icon"])]),e("section",mo,[o[15]||(o[15]=e("div",{class:"doc-section-head"},[e("span",{class:"doc-section-num"},"03"),e("div",null,[e("h2",{class:"doc-section-title"}," Configuration reference "),e("p",{class:"doc-lede"}," Everything below lives on the function record. Secrets are stored encrypted and only decrypt into the worker environment at spawn time. ")])],-1)),e("div",bo,[e("table",go,[o[12]||(o[12]=e("thead",null,[e("tr",null,[e("th",null,"Field"),e("th",{class:"hidden sm:table-cell"}," Purpose "),e("th",null,"Behaviour")])],-1)),e("tbody",null,[(u(),g(_,null,A(le,t=>e("tr",{key:t.field,class:"align-top"},[e("td",yo,[(u(),j(Z(t.icon),{class:P(["w-3.5 h-3.5 shrink-0",t.iconClass])},null,8,["class"])),e("code",null,l(t.field),1)]),e("td",fo,l(t.purpose),1),e("td",ko,l(t.body),1)])),64))])])]),e("div",wo,[o[13]||(o[13]=e("div",{class:"doc-microlabel"}," Set a secret ",-1)),d(n(h),{code:he.value,lang:"bash"},null,8,["code"])]),e("details",Co,[e("summary",xo,[d(n(ee),{class:"w-3.5 h-3.5 transition-transform group-open:rotate-90 text-foreground-muted"}),o[14]||(o[14]=a(" Signed-invoke recipe (HMAC, opt-in) ",-1))]),e("div",To,[d(n(h),{code:ve.value,lang:"bash"},null,8,["code"])])])]),e("section",So,[o[21]||(o[21]=b('
04

SDK from inside a function

The bundled orva module exposes three primitives every function can use without extra dependencies: a per-function key/value store, in-process calls to other Orva functions, and a fire-and-forget background job queue. Routes through the per-process internal token injected at worker spawn time.

orva.kv
put / get / delete / list

Per-function namespace on SQLite, optional TTL.

orva.invoke
invoke(name, payload)

In-process call to another function. 8-deep call cap.

orva.jobs
jobs.enqueue(name, payload)

Fire-and-forget; persisted; retried with exp backoff.

',2)),e("div",_o,[o[16]||(o[16]=e("div",{class:"doc-microlabel"}," KV: get/put with TTL ",-1)),d(n(y),{tabs:be,"storage-key":"docs.sdk.kv"}),o[17]||(o[17]=b('

Browse / inspect / edit / delete / set keys without leaving the dashboard at /web/functions/<name>/kv (or click the KV button in the editor's action bar). REST mirror at GET/PUT/DELETE /api/v1/functions/<id>/kv[/<key>]; MCP tools kv_list / kv_get / kv_put / kv_delete for agents.

',1))]),e("div",Ao,[o[18]||(o[18]=e("div",{class:"doc-microlabel"}," Function-to-function: invoke() ",-1)),d(n(y),{tabs:ge,"storage-key":"docs.sdk.invoke"})]),e("div",Oo,[o[19]||(o[19]=e("div",{class:"doc-microlabel"}," Background jobs: jobs.enqueue() ",-1)),d(n(y),{tabs:ye,"storage-key":"docs.sdk.jobs"})]),d(n($),{icon:n(K),title:"Network mode"},{default:C(()=>[...o[20]||(o[20]=[a(" The SDK reaches orvad over loopback through the host gateway, so the function needs ",-1),e("code",{class:"doc-chip"},'network_mode: "egress"',-1),a(". On the default ",-1),e("code",{class:"doc-chip"},'"none"',-1),a(" the SDK throws ",-1),e("code",{class:"doc-chip"},"OrvaUnavailableError",-1),a(" with a clear hint. ",-1)])]),_:1},8,["icon"])]),e("section",Po,[e("div",jo,[o[32]||(o[32]=e("span",{class:"doc-section-num"},"05",-1)),e("div",null,[o[31]||(o[31]=e("h2",{class:"doc-section-title"}," Schedules ",-1)),e("p",Io,[o[23]||(o[23]=a(" Fire any function on a cron expression. The scheduler runs as part of the orvad process; no external service. Manage from the ",-1)),d(i,{to:"/cron",class:"text-foreground hover:text-white underline decoration-dotted underline-offset-4"},{default:C(()=>[...o[22]||(o[22]=[a("Schedules page",-1)])]),_:1}),o[24]||(o[24]=a(" or via the API. Standard 5-field cron with the usual shorthands (",-1)),o[25]||(o[25]=e("code",{class:"doc-chip"},"@daily",-1)),o[26]||(o[26]=a(", ",-1)),o[27]||(o[27]=e("code",{class:"doc-chip"},"@hourly",-1)),o[28]||(o[28]=a(", ",-1)),o[29]||(o[29]=e("code",{class:"doc-chip"},"*/5 * * * *",-1)),o[30]||(o[30]=a("). ",-1))])])]),d(n(y),{tabs:me.value,"storage-key":"docs.cron"},null,8,["tabs"]),d(n($),{icon:n($e),title:"Cron-fired headers"},{default:C(()=>[...o[33]||(o[33]=[a(" Every cron-triggered invocation arrives at the function with ",-1),e("code",{class:"doc-chip"},"x-orva-trigger: cron",-1),a(" and ",-1),e("code",{class:"doc-chip"},"x-orva-cron-id: cron_…",-1),a(" on the event headers, so user code can branch on origin. ",-1)])]),_:1},8,["icon"])]),e("section",Eo,[e("div",Do,[o[38]||(o[38]=e("span",{class:"doc-section-num"},"06",-1)),e("div",null,[o[37]||(o[37]=e("h2",{class:"doc-section-title"}," Webhooks ",-1)),e("p",Ro,[o[35]||(o[35]=a(" Operator-managed subscriptions for system events. Configure URLs from the ",-1)),d(i,{to:"/webhooks",class:"text-foreground hover:text-white underline decoration-dotted underline-offset-4"},{default:C(()=>[...o[34]||(o[34]=[a("Webhooks page",-1)])]),_:1}),o[36]||(o[36]=a("; Orva delivers signed POSTs to them when matching events fire (deployments, function lifecycle, cron failures, job outcomes). Subscriptions are global, not per-function. ",-1))])])]),d(n(ce)),o[41]||(o[41]=b('
Headers
X-Orva-EventX-Orva-Delivery-IdX-Orva-TimestampX-Orva-Signature
Signature
sha256=hex(hmac(secret, ts.body))

Same shape as Stripe / signed-invoke. Receivers verify with the secret returned at create time.

Retries
5 attemptsexp backoff (≤ 1h)

Receiver must 2xx within 15s.

',1)),e("div",Ho,[e("table",$o,[o[39]||(o[39]=e("thead",null,[e("tr",null,[e("th",null,"Event"),e("th",null,"When it fires")])],-1)),e("tbody",null,[(u(),g(_,null,A(fe,t=>e("tr",{key:t.name},[e("td",Mo,[e("code",null,l(t.name),1)]),e("td",Lo,l(t.when),1)])),64))])])]),e("div",qo,[o[40]||(o[40]=e("div",{class:"doc-microlabel"}," Verify a delivery ",-1)),d(n(y),{tabs:ke,"storage-key":"docs.webhooks.verify"})])]),e("section",No,[o[51]||(o[51]=e("div",{class:"doc-section-head"},[e("span",{class:"doc-section-num"},"07"),e("div",null,[e("h2",{class:"doc-section-title"}," MCP: Model Context Protocol "),e("p",{class:"doc-lede"}," Same API surface the dashboard uses, exposed as 70 tools an agent can call directly. API key permissions scope the available tool set. ")])],-1)),e("div",Bo,[e("div",zo,[o[42]||(o[42]=e("div",{class:"doc-microlabel"}," Endpoint ",-1)),e("div",Ko,[e("code",Uo,l(r.value)+"/mcp",1)])]),o[43]||(o[43]=b('
Auth header
Authorization: Bearer <token>

Or as a fallback: X-Orva-API-Key: <token>

Transport
Streamable HTTPMCP 2025-11-25
',2))]),d(n($),{icon:n(M),title:"Two header formats; same auth"},{default:C(()=>[...o[44]||(o[44]=[a(" Either header works against the same API key store with identical permission gating. ",-1),e("code",{class:"doc-chip"},"Authorization: Bearer",-1),a(" is the MCP / OAuth 2 spec form; every MCP SDK (Claude Code, Claude Desktop, Cursor, mcp-remote, Python ",-1),e("code",{class:"doc-chip"},"mcp",-1),a(") configures it natively, so prefer it for new setups. ",-1),e("code",{class:"doc-chip"},"X-Orva-API-Key",-1),a(" is the same header the REST API accepts; useful when a tool reuses an existing Orva REST integration. Internally both paths SHA-256-hash the token and look it up against the same ",-1),e("code",{class:"doc-chip"},"api_keys",-1),a(" table. ",-1)])]),_:1},8,["icon"]),e("div",Fo,[e("div",Xo,[d(n(M),{class:"w-4 h-4 shrink-0 text-foreground-muted"}),R.value?(u(),g("span",Go,[o[47]||(o[47]=a(" Token minted: ",-1)),e("code",Wo,l(_e.value)+"…",1),o[48]||(o[48]=a(" Shown once, copy now. ",-1))])):(u(),g("span",Vo,[o[45]||(o[45]=a(" Snippets show ",-1)),e("code",{class:"doc-chip"},l(oe)),o[46]||(o[46]=a(". Mint a token to substitute it everywhere. ",-1))]))]),e("button",{class:"doc-token-btn",disabled:H.value,onClick:Ae},[d(n(M),{class:"w-3.5 h-3.5"}),a(" "+l(R.value?"Mint another":H.value?"Minting…":"Generate token"),1)],8,Yo)]),d(n(y),{tabs:Oe.value,"storage-key":"docs.mcp.install"},null,8,["tabs"]),e("details",Jo,[e("summary",Zo,[d(n(ee),{class:"w-3.5 h-3.5 transition-transform group-open:rotate-90 text-foreground-muted"}),o[49]||(o[49]=a(" More clients (Cursor, VS Code, Codex CLI, OpenCode, Zed, Windsurf, ChatGPT, manual config) ",-1))]),e("div",Qo,[d(n(y),{tabs:Pe.value,"storage-key":"docs.mcp.install.more"},null,8,["tabs"]),o[50]||(o[50]=e("div",{class:"doc-microlabel pt-1"}," Hand-edited config files ",-1)),d(n(y),{tabs:Ie.value,"storage-key":"docs.mcp.manual"},null,8,["tabs"])])])]),e("section",es,[o[52]||(o[52]=b('
08

System prompt for AI assistants

Paste the prompt below into ChatGPT, Claude, Gemini, Cursor, Copilot, or any other AI tool to teach it Orva's full surface Handler contract, runtimes, sandbox limits, the in-sandbox orva SDK (kv / invoke / jobs), cron triggers, system-event webhooks, auth modes, and production patterns. The model then turns "describe what I want" into a pasteable handler on the first try.

',1)),e("div",os,[e("button",{class:P(["ai-copy-btn",{copied:D.value}]),onClick:te},[D.value?(u(),j(n(B),{key:0,class:"w-3.5 h-3.5"})):(u(),j(n(z),{key:1,class:"w-3.5 h-3.5"})),a(" "+l(D.value?"Copied":"Copy system prompt"),1)],2)]),e("div",{class:P(["prompt-collapse",{expanded:S.value}])},[d(n(h),{code:n(se),lang:"text"},null,8,["code"]),S.value?Me("",!0):(u(),g("div",ss))],2),e("button",{class:"prompt-expand-btn","aria-expanded":S.value,onClick:o[0]||(o[0]=t=>S.value=!S.value)},[d(n(Ke),{class:P(["w-3.5 h-3.5 transition-transform",{"rotate-180":S.value}])},null,8,["class"]),a(" "+l(S.value?"Collapse system prompt":"Expand full system prompt (~400 lines)"),1)],8,ts)]),e("section",as,[o[54]||(o[54]=b('
09

Tracing

Every invocation chain is recorded as a causal trace. automatically, with zero changes to your function code. HTTP requests, F2F invokes, jobs, cron, inbound webhooks, and replays all stitch into the same tree. The dashboard renders it as a waterfall at /traces.

Each execution row IS a span. Spans share a trace_id; child spans point at their parent via parent_span_id. You don't instantiate spans, you don't import a tracer; you just write your handler and the platform plumbs IDs through every internal hop.

',2)),d(n(ne)),o[55]||(o[55]=e("h3",{class:"doc-h3"},"What user code sees",-1)),o[56]||(o[56]=e("p",{class:"doc-prose"}," Two env vars are stamped per invocation. Read them only if you want to log the trace_id alongside your own messages; they're optional. ",-1)),d(n(h),{code:_s,lang:"text"}),o[57]||(o[57]=e("h3",{class:"doc-h3"},"Automatic propagation",-1)),o[58]||(o[58]=e("p",{class:"doc-prose"},[a(" When a function calls another via the SDK, the trace context flows through automatically. The called function becomes a child span of the caller; both share the same "),e("code",{class:"doc-chip"},"trace_id"),a(". ")],-1)),d(n(h),{code:As,lang:"js"}),o[59]||(o[59]=b('

Job enqueues work the same way: orva.jobs.enqueue() records the trace context on the job row. When the scheduler picks the job up later, the resulting execution lands in the same trace as the function that enqueued it, even if the gap is hours or days.

Triggers

Each span carries a trigger label so the UI can show how the chain started.

',3)),e("div",ns,[e("table",cs,[o[53]||(o[53]=e("thead",null,[e("tr",null,[e("th",null,"Trigger"),e("th",null,"Meaning")])],-1)),e("tbody",null,[(u(),g(_,null,A(we,t=>e("tr",{key:t.name},[e("td",ds,[e("code",null,l(t.name),1)]),e("td",rs,l(t.desc),1)])),64))])])]),o[60]||(o[60]=e("h3",{class:"doc-h3"},"External correlation (W3C traceparent)",-1)),o[61]||(o[61]=e("p",{class:"doc-prose"},[a(" Send a standard "),e("code",{class:"doc-chip"},"traceparent"),a(" header on the inbound HTTP request and Orva makes its trace a child of yours. The same trace_id is echoed back as "),e("code",{class:"doc-chip"},"X-Trace-Id"),a(" on every response, so external systems can correlate without parsing bodies. ")],-1)),d(n(h),{code:Os,lang:"bash"}),o[62]||(o[62]=b('

Outlier detection

Each function maintains an in-memory rolling P95 baseline over its last 100 successful warm executions. An invocation is flagged as an outlier when it has at least 20 baseline samples AND its duration exceeds P95 × 2. Cold starts and errors are excluded from the baseline so a flapping function can't drag it down. The flag and baseline P95 are stored on the execution row and rendered as an amber flag icon next to the span.

Where to look

  • /traces: list of recent traces, filterable by function / status / outlier-only.
  • /traces/:id: waterfall + per-span detail. Click a span to jump to its execution in the Invocations log.
  • GET /api/v1/traces/{id}: full span tree as JSON. Pair with list_traces / get_trace MCP tools for AI agents.
  • GET /api/v1/functions/{id}/baseline: current P95/P99/mean for a function.
',4))]),e("section",is,[o[64]||(o[64]=b('
10

Errors & recovery

Every error response uses the same envelope so log scrapers and retries can match on code. Deploys are content-addressed; rollback retargets the active version pointer and refreshes warm workers.

',1)),d(n(h),{code:Ps,lang:"json"}),e("div",ls,[e("table",ps,[o[63]||(o[63]=e("thead",null,[e("tr",null,[e("th",null,"Code"),e("th",null,"When you see it")])],-1)),e("tbody",null,[(u(),g(_,null,A(Ce,t=>e("tr",{key:t.code},[e("td",us,[e("code",null,l(t.code),1)]),e("td",hs,l(t.when),1)])),64))])])])]),e("section",vs,[o[81]||(o[81]=b('
11

CLI

orva is a single static binary that talks to a remote (or local) Orva server over HTTPS. Same binary as the daemon, orva serve starts a server, every other subcommand is a CLI client. Drop it on operator laptops, CI runners, or anywhere bash runs.

Install (server included)
curl … install.sh | sh

Full install: daemon + nsjail + rootfs + CLI.

Install (CLI only)
install.sh --cli-only

~10 MB binary at /usr/local/bin/orva. No service.

Inside Docker
docker exec orva orva …

CLI auto-authed via ~/.orva/config.yaml.

Authenticate

',3)),e("p",ms,[o[66]||(o[66]=a(" The CLI reads ",-1)),o[67]||(o[67]=e("code",{class:"doc-chip"},"~/.orva/config.yaml",-1)),o[68]||(o[68]=a(" for ",-1)),o[69]||(o[69]=e("code",{class:"doc-chip"},"endpoint",-1)),o[70]||(o[70]=a(" + ",-1)),o[71]||(o[71]=e("code",{class:"doc-chip"},"api_key",-1)),o[72]||(o[72]=a(". Generate a key from ",-1)),d(i,{to:"/api-keys",class:"text-foreground hover:text-white underline decoration-dotted underline-offset-4"},{default:C(()=>[...o[65]||(o[65]=[a("Keys",-1)])]),_:1}),o[73]||(o[73]=a(" in the dashboard, then: ",-1))]),d(n(h),{code:js,lang:"bash"}),o[82]||(o[82]=e("h3",{class:"doc-h3"},"Command index",-1)),e("div",bs,[e("table",gs,[o[74]||(o[74]=e("thead",null,[e("tr",null,[e("th",null,"Command"),e("th",null,"Subcommands"),e("th",{class:"hidden md:table-cell"},"Purpose")])],-1)),e("tbody",null,[(u(),g(_,null,A(xe,t=>e("tr",{key:t.cmd},[e("td",ys,[e("code",null,"orva "+l(t.cmd),1)]),e("td",fs,l(t.subs),1),e("td",ks,l(t.purpose),1)])),64))])])]),o[83]||(o[83]=e("h3",{class:"doc-h3"},"Common recipes",-1)),e("div",ws,[o[75]||(o[75]=e("div",{class:"doc-microlabel"},"Deploy a function from a directory",-1)),d(n(h),{code:Is,lang:"bash"})]),e("div",Cs,[o[76]||(o[76]=e("div",{class:"doc-microlabel"},"Invoke + tail logs",-1)),d(n(h),{code:Es,lang:"bash"})]),e("div",xs,[o[77]||(o[77]=e("div",{class:"doc-microlabel"},"Manage KV state",-1)),d(n(h),{code:Ds,lang:"bash"})]),e("div",Ts,[o[78]||(o[78]=e("div",{class:"doc-microlabel"},"Secrets, cron, jobs, webhooks",-1)),d(n(h),{code:Rs,lang:"bash"})]),e("div",Ss,[o[79]||(o[79]=e("div",{class:"doc-microlabel"},"System health, metrics, vacuum",-1)),d(n(h),{code:Hs,lang:"bash"})]),d(n($),{icon:n(M),title:"Shell completion"},{default:C(()=>[...o[80]||(o[80]=[a(" Generate completion for your shell: ",-1),e("code",{class:"doc-chip"},"orva completion bash | sudo tee /etc/bash_completion.d/orva",-1),a(", or ",-1),e("code",{class:"doc-chip"},"zsh",-1),a(" / ",-1),e("code",{class:"doc-chip"},"fish",-1),a(" / ",-1),e("code",{class:"doc-chip"},"powershell",-1),a(". Tab-completes commands, subcommands, and flags. ",-1)])]),_:1},8,["icon"])])])}}};export{Ys as default}; diff --git a/backend/internal/server/ui_dist/assets/Drawer-B7DqDJXO.js b/backend/internal/server/ui_dist/assets/Drawer-B7DqDJXO.js new file mode 100644 index 0000000..c1ac9a9 --- /dev/null +++ b/backend/internal/server/ui_dist/assets/Drawer-B7DqDJXO.js @@ -0,0 +1 @@ +import{H as h,I as p,o as y,y as x,j as a,n as k,d as r,h as m,a as n,b as o,N as f,R as v,S as V,Q as d,f as _,ac as g,g as l,U as S,r as B,k as C,t as N,L as D}from"./index-CmY58qNN.js";const E={key:0,class:"fixed inset-0 z-50 pointer-events-none"},T={class:"px-5 py-3 border-b border-border flex items-center justify-between shrink-0"},$={class:"text-sm font-medium text-white truncate"},K={class:"flex-1 overflow-y-auto scrollable"},L={key:0,class:"px-5 py-3 border-t border-border shrink-0"},j={__name:"Drawer",props:{modelValue:{type:Boolean,default:!1},title:{type:String,default:""},width:{type:String,default:"560px"}},emits:["update:modelValue"],setup(t,{emit:b}){const i=t,w=b,c=B(null),s=()=>w("update:modelValue",!1);p(()=>i.modelValue,async e=>{e&&(await D(),c.value?.focus?.())});const u=e=>{e.key==="Escape"&&i.modelValue&&s()};return y(()=>window.addEventListener("keydown",u)),x(()=>window.removeEventListener("keydown",u)),(e,z)=>(a(),k(S,{to:"body"},[r(f,{name:"drawer-fade"},{default:m(()=>[t.modelValue?(a(),n("div",E,[o("div",{class:"absolute inset-0 pointer-events-auto",onClick:s}),r(f,{name:"drawer-slide"},{default:m(()=>[t.modelValue?(a(),n("div",{key:0,class:"absolute pointer-events-auto bg-background flex flex-col inset-x-0 bottom-0 max-h-[85dvh] border-t border-border rounded-t-lg pb-safe sm:inset-x-auto sm:right-0 sm:top-0 sm:bottom-0 sm:max-h-none sm:border-t-0 sm:border-l sm:rounded-none sm:pb-0 sm:w-[var(--drawer-w,560px)]",style:V({"--drawer-w":t.width}),onKeydown:v(s,["esc"]),tabindex:"-1",ref_key:"root",ref:c},[o("header",T,[o("div",$,[d(e.$slots,"title",{},()=>[C(N(t.title),1)],!0)]),o("button",{class:"text-foreground-muted hover:text-white transition-colors touch-expand-iconbtn -mr-1",onClick:s,"aria-label":"Close"},[r(_(g),{class:"w-4 h-4"})])]),o("div",K,[d(e.$slots,"default",{},void 0,!0)]),e.$slots.footer?(a(),n("footer",L,[d(e.$slots,"footer",{},void 0,!0)])):l("",!0)],36)):l("",!0)]),_:3})])):l("",!0)]),_:3})]))}},U=h(j,[["__scopeId","data-v-bda4ef6c"]]);export{U as D}; diff --git a/backend/internal/server/ui_dist/assets/Drawer-DFsQitq5.js b/backend/internal/server/ui_dist/assets/Drawer-DFsQitq5.js deleted file mode 100644 index bf8e24b..0000000 --- a/backend/internal/server/ui_dist/assets/Drawer-DFsQitq5.js +++ /dev/null @@ -1 +0,0 @@ -import{K as h,L as p,o as y,y as x,j as a,n as k,d as r,h as m,a as n,b as o,T as f,a5 as v,aG as V,aH as d,f as _,a3 as g,g as l,aI as B,r as C,k as S,t as T,af as D}from"./index-WXhXpu06.js";const E={key:0,class:"fixed inset-0 z-50 pointer-events-none"},K={class:"px-5 py-3 border-b border-border flex items-center justify-between shrink-0"},N={class:"text-sm font-medium text-white truncate"},$={class:"flex-1 overflow-y-auto scrollable"},L={key:0,class:"px-5 py-3 border-t border-border shrink-0"},j={__name:"Drawer",props:{modelValue:{type:Boolean,default:!1},title:{type:String,default:""},width:{type:String,default:"560px"}},emits:["update:modelValue"],setup(t,{emit:b}){const i=t,w=b,c=C(null),s=()=>w("update:modelValue",!1);p(()=>i.modelValue,async e=>{e&&(await D(),c.value?.focus?.())});const u=e=>{e.key==="Escape"&&i.modelValue&&s()};return y(()=>window.addEventListener("keydown",u)),x(()=>window.removeEventListener("keydown",u)),(e,z)=>(a(),k(B,{to:"body"},[r(f,{name:"drawer-fade"},{default:m(()=>[t.modelValue?(a(),n("div",E,[o("div",{class:"absolute inset-0 pointer-events-auto",onClick:s}),r(f,{name:"drawer-slide"},{default:m(()=>[t.modelValue?(a(),n("div",{key:0,class:"absolute pointer-events-auto bg-background flex flex-col inset-x-0 bottom-0 max-h-[85dvh] border-t border-border rounded-t-lg pb-safe sm:inset-x-auto sm:right-0 sm:top-0 sm:bottom-0 sm:max-h-none sm:border-t-0 sm:border-l sm:rounded-none sm:pb-0 sm:w-[var(--drawer-w,560px)]",style:V({"--drawer-w":t.width}),onKeydown:v(s,["esc"]),tabindex:"-1",ref_key:"root",ref:c},[o("header",K,[o("div",N,[d(e.$slots,"title",{},()=>[S(T(t.title),1)],!0)]),o("button",{class:"text-foreground-muted hover:text-white transition-colors touch-expand-iconbtn -mr-1",onClick:s,"aria-label":"Close"},[r(_(g),{class:"w-4 h-4"})])]),o("div",$,[d(e.$slots,"default",{},void 0,!0)]),e.$slots.footer?(a(),n("footer",L,[d(e.$slots,"footer",{},void 0,!0)])):l("",!0)],36)):l("",!0)]),_:3})])):l("",!0)]),_:3})]))}},G=h(j,[["__scopeId","data-v-bda4ef6c"]]);export{G as D}; diff --git a/backend/internal/server/ui_dist/assets/Editor-D20sudg4.css b/backend/internal/server/ui_dist/assets/Editor-ChfgDDB5.css similarity index 52% rename from backend/internal/server/ui_dist/assets/Editor-D20sudg4.css rename to backend/internal/server/ui_dist/assets/Editor-ChfgDDB5.css index 5c3dfc6..4580763 100644 --- a/backend/internal/server/ui_dist/assets/Editor-D20sudg4.css +++ b/backend/internal/server/ui_dist/assets/Editor-ChfgDDB5.css @@ -1 +1 @@ -.panel-btn[data-v-c85cd9a2]{position:relative;display:inline-flex;align-items:center;gap:.375rem;padding:.375rem .625rem;border-radius:.375rem;border:1px solid var(--color-border);background-color:var(--color-surface-hover);color:var(--color-foreground-muted);font-size:11px;font-weight:500;white-space:nowrap;transition:color .15s ease,background-color .15s ease,border-color .15s ease}.panel-btn[data-v-c85cd9a2]:before{content:"";position:absolute;inset-inline:0;inset-block:-8px}.panel-btn[data-v-c85cd9a2]:hover{color:var(--color-foreground);border-color:var(--color-foreground-muted)}.panel-btn[data-v-c85cd9a2]:disabled{opacity:.5;cursor:not-allowed}.menu-item[data-v-c85cd9a2]{display:flex;width:100%;align-items:center;gap:.5rem;min-height:44px;padding:.5rem .75rem;font-size:12px;color:var(--color-foreground);background-color:transparent;border:0;text-align:left;cursor:pointer;transition:background-color .12s ease}.menu-item[data-v-c85cd9a2]:hover,.menu-item[data-v-c85cd9a2]:focus-visible{background-color:var(--color-surface-hover);outline:none}.menu-item+.menu-item[data-v-c85cd9a2]{border-top:1px solid var(--color-border)}.run-btn[data-v-c85cd9a2]{display:inline-flex;align-items:center;gap:.3rem;padding:.2rem .55rem;border-radius:.3rem;border:1px solid rgba(85,63,131,.55);background:#553f832e;color:var(--color-foreground);font-size:11px;font-weight:500;cursor:pointer;transition:background .12s ease,border-color .12s ease,color .12s ease}.run-btn[data-v-c85cd9a2]:hover:not(:disabled){background:#553f8352;border-color:var(--color-primary);color:#fff}.run-btn[data-v-c85cd9a2]:disabled{opacity:.4;cursor:not-allowed}.run-spinner[data-v-c85cd9a2]{width:.65rem;height:.65rem;border-radius:999px;border:1.5px solid currentColor;border-top-color:transparent;animation:run-spin-c85cd9a2 .7s linear infinite}@keyframes run-spin-c85cd9a2{to{transform:rotate(360deg)}} +.panel-btn[data-v-4e8b616a]{position:relative;display:inline-flex;align-items:center;gap:.375rem;padding:.375rem .625rem;border-radius:.375rem;border:1px solid var(--color-border);background-color:var(--color-surface-hover);color:var(--color-foreground-muted);font-size:11px;font-weight:500;white-space:nowrap;transition:color .15s ease,background-color .15s ease,border-color .15s ease}.panel-btn[data-v-4e8b616a]:before{content:"";position:absolute;inset-inline:0;inset-block:-8px}.panel-btn[data-v-4e8b616a]:hover{color:var(--color-foreground);border-color:var(--color-foreground-muted)}.panel-btn[data-v-4e8b616a]:disabled{opacity:.5;cursor:not-allowed}.menu-item[data-v-4e8b616a]{display:flex;width:100%;align-items:center;gap:.5rem;min-height:44px;padding:.5rem .75rem;font-size:12px;color:var(--color-foreground);background-color:transparent;border:0;text-align:left;cursor:pointer;transition:background-color .12s ease}.menu-item[data-v-4e8b616a]:hover,.menu-item[data-v-4e8b616a]:focus-visible{background-color:var(--color-surface-hover);outline:none}.menu-item+.menu-item[data-v-4e8b616a]{border-top:1px solid var(--color-border)}.run-btn[data-v-4e8b616a]{display:inline-flex;align-items:center;gap:.3rem;padding:.2rem .55rem;border-radius:.3rem;border:1px solid rgba(85,63,131,.55);background:#553f832e;color:var(--color-foreground);font-size:11px;font-weight:500;cursor:pointer;transition:background .12s ease,border-color .12s ease,color .12s ease}.run-btn[data-v-4e8b616a]:hover:not(:disabled){background:#553f8352;border-color:var(--color-primary);color:#fff}.run-btn[data-v-4e8b616a]:disabled{opacity:.4;cursor:not-allowed}.run-spinner[data-v-4e8b616a]{width:.65rem;height:.65rem;border-radius:999px;border:1.5px solid currentColor;border-top-color:transparent;animation:run-spin-4e8b616a .7s linear infinite}@keyframes run-spin-4e8b616a{to{transform:rotate(360deg)}} diff --git a/backend/internal/server/ui_dist/assets/Editor-CuVdxjUJ.js b/backend/internal/server/ui_dist/assets/Editor-CqEjsyD4.js similarity index 77% rename from backend/internal/server/ui_dist/assets/Editor-CuVdxjUJ.js rename to backend/internal/server/ui_dist/assets/Editor-CqEjsyD4.js index c636abb..6ec7d5b 100644 --- a/backend/internal/server/ui_dist/assets/Editor-CuVdxjUJ.js +++ b/backend/internal/server/ui_dist/assets/Editor-CqEjsyD4.js @@ -1,5 +1,5 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/CodeEditor-Do1Z2Nkh.js","assets/index-BOWx3BJu.js","assets/index-WXhXpu06.js","assets/index-DVPSQHGf.css","assets/CodeEditor-tn0RQdqM.css"])))=>i.map(i=>d[i]); -import{c as Je,K as po,C as fo,L as he,o as vo,M as gt,a as l,b as o,t as p,d,f,e as _,v as O,g as h,k as u,N as ho,W as go,h as g,_ as M,n as le,F as S,p as C,s as oe,Q as bo,R as Oe,J as T,r as v,q as E,U as yo,V as xo,X as _o,Y as wo,Z as ko,i as So,$ as Co,a0 as To,a1 as Eo,j as i,a2 as Oo,w as Ro,a3 as $e,a4 as jo,a5 as No,a6 as Do,a7 as Ao,a8 as Po,a9 as Io,aa as Mo,z as ge}from"./index-WXhXpu06.js";import{_ as re,a as de,S as Vo}from"./Modal--oERTKOu.js";import{c as Lo}from"./clipboard-CmSw2rR-.js";import{c as $o}from"./aiPrompts-Dgb3jxRL.js";import{d as Uo}from"./rollbackDiff-Cvt2Ss82.js";import{F as bt,S as Ue,P as yt}from"./settings-2-2knNMPAW.js";import{C as be}from"./chevron-down-CtdSqW4P.js";import{V as xt}from"./variable-wg-kDh52.js";import{K as _t}from"./key-round-BMBM5azh.js";import{B as wt}from"./book-open-Jrxx9Bqu.js";import{C as qo}from"./check-CuO1EVch.js";import{C as Bo}from"./copy-HWkx-vox.js";import{P as kt}from"./play-l-CnPJiF.js";import{S as zo}from"./sparkles-Bj1MS2Kp.js";import{C as qe,G as Fo}from"./git-compare-CqfimOR1.js";import{T as Be}from"./trash-2-dFLn8UYZ.js";import{G as St}from"./globe-DI_Jf14z.js";import{L as Ho}from"./lock-CDky0HD2.js";import{R as Go}from"./rotate-ccw-Cx2X0X0k.js";import{T as Jo}from"./terminal-DbYqyx_n.js";const Ko=Je("database",[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3",key:"msslwz"}],["path",{d:"M3 5V19A9 3 0 0 0 21 19V5",key:"1wlel7"}],["path",{d:"M3 12A9 3 0 0 0 21 12",key:"mv7ke4"}]]);const Ct=Je("layers",[["path",{d:"M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z",key:"zw3jo"}],["path",{d:"M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12",key:"1wduqc"}],["path",{d:"M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17",key:"kqbvx6"}]]);const Tt=Je("shuffle",[["path",{d:"m18 14 4 4-4 4",key:"10pe0f"}],["path",{d:"m18 2 4 4-4 4",key:"pucp1d"}],["path",{d:"M2 18h1.973a4 4 0 0 0 3.3-1.7l5.454-8.6a4 4 0 0 1 3.3-1.7H22",key:"1ailkh"}],["path",{d:"M2 6h1.972a4 4 0 0 1 3.6 2.2",key:"km57vx"}],["path",{d:"M22 18h-6.041a4 4 0 0 1-3.3-1.8l-.359-.45",key:"os18l9"}]]),Wo=["amber","arctic","aurora","bold","brave","breezy","bright","brisk","calm","celestial","cobalt","cosmic","crimson","crisp","crystal","dapper","dazzling","deep","eager","ember","fearless","feisty","fierce","flaming","fluent","fluorescent","frosty","gentle","glacial","golden","graceful","happy","hazy","icy","indigo","jade","jolly","jovial","keen","kindred","lavender","lively","lucent","lunar","magenta","magnetic","merry","midnight","mighty","mellow","mossy","mystic","neon","nimble","noble","obsidian","opal","pearl","peppy","pixel","plucky","plush","polar","prime","quartz","quick","quiet","radiant","rapid","rare","roaming","rosy","royal","rugged","runic","rustic","sapphire","scarlet","sharp","silent","silken","silver","sleek","smooth","snowy","snug","solar","sonic","spry","starlit","stellar","sturdy","sublime","sunny","svelte","swift","tame","tender","thunder","tidal","topaz","tropic","turquoise","twilight","urban","velvet","verdant","vibrant","violet","vivid","warm","whisper","wild","wise","witty","woven","zesty","zen"],Yo=["albatross","amber","antler","apricot","archer","arrow","atlas","aurora","badger","bayou","beacon","bison","blossom","bramble","breeze","cactus","canyon","caravan","cedar","cliff","comet","compass","coral","cosmos","cypress","dawn","delta","dolphin","drift","dune","eagle","ember","fable","falcon","fern","fjord","flame","flint","forest","galaxy","garnet","geyser","glacier","glade","glint","gorge","gull","harbor","haven","horizon","iceberg","iris","jaguar","jetty","jungle","kelp","kestrel","kettle","kraken","lagoon","lantern","lark","ledge","lily","lighthouse","lupine","lynx","maple","meadow","meridian","meteor","mirage","mistral","monsoon","moon","moss","mountain","nebula","oak","oasis","ocean","orchid","osprey","otter","panda","panther","parrot","pebble","phoenix","pine","pinion","pixel","planet","plume","pond","poppy","prairie","puffin","puma","quartz","quasar","quill","rapids","raven","reef","ridge","river","robin","rune","sage","satellite","savanna","sequoia","shadow","signal","silo","sky","sloth","snow","sparrow","spire","star","stream","summit","swan","tempest","thicket","thistle","thunder","tide","tiger","totem","tower","tundra","twilight","twister","valley","vortex","walnut","wave","whale","whisper","wildflower","willow","wolf","wren","zenith","zephyr"],Et=Re=>Re[Math.floor(Math.random()*Re.length)];function Ot(){return`${Et(Wo)}-${Et(Yo)}`}const He=`import json +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/CodeEditor-se_FXC0j.js","assets/index-BOWx3BJu.js","assets/index-CmY58qNN.js","assets/index-CW3Fy24v.css","assets/CodeEditor-tn0RQdqM.css"])))=>i.map(i=>d[i]); +import{c as Je,H as po,G as fo,I as he,o as vo,J as gt,a as l,b as o,t as p,d,f,e as _,v as O,g as h,k as u,a2 as ho,a3 as go,h as g,_ as V,n as le,F as S,p as C,s as oe,a4 as bo,V as Oe,D as T,r as v,q as E,a5 as yo,a6 as xo,E as _o,a7 as wo,a8 as ko,i as So,a9 as Co,aa as To,ab as Eo,j as i,K as Oo,w as Ro,ac as $e,ad as jo,R as No,ae as Do,af as Ao,ag as Po,ah as Io,ai as Vo,z as ge}from"./index-CmY58qNN.js";import{_ as de}from"./Input-B5GdDd-T.js";import{_ as re}from"./Modal-zN5wBHcp.js";import{c as Mo}from"./clipboard-CmSw2rR-.js";import{c as Lo}from"./aiPrompts-Dgb3jxRL.js";import{d as $o}from"./rollbackDiff-Cvt2Ss82.js";import{F as bt,P as yt}from"./package-DGq9n3E3.js";import{S as Ue}from"./settings-2-CJPaZu6R.js";import{C as be}from"./chevron-down-BMYhN6hn.js";import{V as xt}from"./variable-CG75qovQ.js";import{K as _t}from"./key-round-ByFVPOPP.js";import{B as wt}from"./book-open-DME4yN8Y.js";import{C as Uo}from"./check-z8qyH5P-.js";import{C as qo}from"./copy-BzujsGZw.js";import{P as kt}from"./play-DZQVXk9D.js";import{S as Bo}from"./sparkles-B3_cifRe.js";import{C as qe,G as zo}from"./git-compare-BYn-27do.js";import{T as Be}from"./trash-2-BJzRpJ7Y.js";import{G as St}from"./globe-D5WsrNJb.js";import{L as Fo}from"./lock-DHycfcno.js";import{S as Ho}from"./shield-check-DChOQFDR.js";import{R as Go}from"./rotate-ccw-DQrtkmfg.js";import{T as Jo}from"./terminal-DzrvMdtW.js";const Ko=Je("database",[["ellipse",{cx:"12",cy:"5",rx:"9",ry:"3",key:"msslwz"}],["path",{d:"M3 5V19A9 3 0 0 0 21 19V5",key:"1wlel7"}],["path",{d:"M3 12A9 3 0 0 0 21 12",key:"mv7ke4"}]]);const Ct=Je("layers",[["path",{d:"M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z",key:"zw3jo"}],["path",{d:"M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12",key:"1wduqc"}],["path",{d:"M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17",key:"kqbvx6"}]]);const Tt=Je("shuffle",[["path",{d:"m18 14 4 4-4 4",key:"10pe0f"}],["path",{d:"m18 2 4 4-4 4",key:"pucp1d"}],["path",{d:"M2 18h1.973a4 4 0 0 0 3.3-1.7l5.454-8.6a4 4 0 0 1 3.3-1.7H22",key:"1ailkh"}],["path",{d:"M2 6h1.972a4 4 0 0 1 3.6 2.2",key:"km57vx"}],["path",{d:"M22 18h-6.041a4 4 0 0 1-3.3-1.8l-.359-.45",key:"os18l9"}]]),Wo=["amber","arctic","aurora","bold","brave","breezy","bright","brisk","calm","celestial","cobalt","cosmic","crimson","crisp","crystal","dapper","dazzling","deep","eager","ember","fearless","feisty","fierce","flaming","fluent","fluorescent","frosty","gentle","glacial","golden","graceful","happy","hazy","icy","indigo","jade","jolly","jovial","keen","kindred","lavender","lively","lucent","lunar","magenta","magnetic","merry","midnight","mighty","mellow","mossy","mystic","neon","nimble","noble","obsidian","opal","pearl","peppy","pixel","plucky","plush","polar","prime","quartz","quick","quiet","radiant","rapid","rare","roaming","rosy","royal","rugged","runic","rustic","sapphire","scarlet","sharp","silent","silken","silver","sleek","smooth","snowy","snug","solar","sonic","spry","starlit","stellar","sturdy","sublime","sunny","svelte","swift","tame","tender","thunder","tidal","topaz","tropic","turquoise","twilight","urban","velvet","verdant","vibrant","violet","vivid","warm","whisper","wild","wise","witty","woven","zesty","zen"],Yo=["albatross","amber","antler","apricot","archer","arrow","atlas","aurora","badger","bayou","beacon","bison","blossom","bramble","breeze","cactus","canyon","caravan","cedar","cliff","comet","compass","coral","cosmos","cypress","dawn","delta","dolphin","drift","dune","eagle","ember","fable","falcon","fern","fjord","flame","flint","forest","galaxy","garnet","geyser","glacier","glade","glint","gorge","gull","harbor","haven","horizon","iceberg","iris","jaguar","jetty","jungle","kelp","kestrel","kettle","kraken","lagoon","lantern","lark","ledge","lily","lighthouse","lupine","lynx","maple","meadow","meridian","meteor","mirage","mistral","monsoon","moon","moss","mountain","nebula","oak","oasis","ocean","orchid","osprey","otter","panda","panther","parrot","pebble","phoenix","pine","pinion","pixel","planet","plume","pond","poppy","prairie","puffin","puma","quartz","quasar","quill","rapids","raven","reef","ridge","river","robin","rune","sage","satellite","savanna","sequoia","shadow","signal","silo","sky","sloth","snow","sparrow","spire","star","stream","summit","swan","tempest","thicket","thistle","thunder","tide","tiger","totem","tower","tundra","twilight","twister","valley","vortex","walnut","wave","whale","whisper","wildflower","willow","wolf","wren","zenith","zephyr"],Et=Re=>Re[Math.floor(Math.random()*Re.length)];function Ot(){return`${Et(Wo)}-${Et(Yo)}`}const He=`import json def handler(event): @@ -1274,23 +1274,23 @@ exports.handler = async (event) => { return { statusCode: 200, body: JSON.stringify({ hits, stats }) } } -`,jt=[{id:"node-http-hello",category:"Starter",label:"HTTP Hello",description:"Minimal POST /. Echoes a name from the JSON body.",code:Ge,deps:""},{id:"node-discord-webhook",category:"Webhooks",label:"Discord webhook",description:"Ed25519 verify + slash command response.",code:is,deps:ls},{id:"node-jwt-validator",category:"Auth",label:"JWT validator",description:"Verify HS256 token from Authorization header.",code:ds,deps:""},{id:"node-oauth-callback",category:"Auth",label:"OAuth callback",description:"Exchange authorization_code for an access token.",code:us,deps:""},{id:"node-md-to-html",category:"Utility",label:"Markdown → HTML",description:"Render markdown via marked; safe inline HTML.",code:cs,deps:ms},{id:"node-rest-proxy",category:"Utility",label:"REST proxy",description:"Forward to upstream + inject Authorization header.",code:ps,deps:""},{id:"node-email-digest",category:"Scheduled",label:"Email digest",cron:!0,description:"Send a daily summary via SMTP (nodemailer).",code:fs,deps:vs},{id:"node-url-shortener",category:"Utility",label:"URL shortener",description:"POST creates a slug, GET redirects. Uses Orva KV.",code:hs,deps:""},{id:"node-counter-cas",category:"Showcase",label:"v0.6 SDK counter (kv.incr + kv.cas)",description:"Atomic counter and a CAS-retry loop for richer state. Demonstrates v0.6 race-safe primitives and structured logging end-to-end.",code:_s,deps:""},{id:"ts-hello",category:"Starter",label:"TypeScript hello",description:"Typed handler compiled at deploy time via tsc. Outputs to dist/handler.js.",code:gs,deps:bs,extras:{"tsconfig.json":ys},entrypoint:"handler.ts"}],ze={python313:Rt,python314:Rt,node22:jt,node24:jt},Fe={python313:He,python314:He,node22:Ge,node24:Ge},Nt=["Starter","Webhooks","Auth","Utility","Scheduled","Showcase"],ws={class:"flex flex-col h-full"},ks={class:"sr-only"},Ss={class:"flex flex-col sm:flex-row sm:flex-wrap sm:items-center gap-2 pb-3 border-b border-border"},Cs={class:"flex items-center gap-2 sm:mr-auto min-w-0 w-full sm:w-auto"},Ts=["disabled"],Es={class:"text-[11px] text-foreground-muted font-medium tracking-tight shrink-0"},Os={class:"flex flex-wrap items-center gap-2 sm:contents"},Rs=["aria-expanded"],js={key:0,class:"absolute right-0 mt-1 z-30 min-w-[210px] bg-background border border-border rounded-md shadow-xl overflow-hidden",role:"menu"},Ns={key:0,class:"text-[10px] text-foreground-muted tabular-nums"},Ds={key:0,class:"text-[10px] text-foreground-muted tabular-nums"},As={class:"text-[10px] text-foreground-muted tabular-nums"},Ps=["aria-expanded"],Is={key:0,class:"absolute right-0 mt-1 z-30 min-w-[210px] bg-background border border-border rounded-md shadow-xl overflow-hidden",role:"menu"},Ms={key:0,class:"flex items-center gap-2 px-2 py-1.5 mt-2 border border-border bg-surface rounded text-xs"},Vs={class:"font-mono text-white truncate flex-1 min-w-0"},Ls={class:"flex-1 flex flex-col min-h-0 mt-3 bg-background border border-border rounded-lg overflow-hidden shadow-sm"},$s={class:"h-9 border-b border-border flex items-center justify-between px-4 bg-surface shrink-0"},Us={class:"text-xs font-mono text-foreground-muted flex items-center gap-2"},qs={class:"text-white"},Bs={key:0,class:"text-foreground-muted"},zs={class:"text-[10px] text-foreground-muted font-mono"},Fs={class:"mt-3 bg-background border border-border rounded-lg overflow-hidden shrink-0"},Hs={class:"h-9 border-b border-border flex items-center px-2 bg-surface"},Gs=["onClick"],Js={key:0,class:"ml-1 text-[10px] px-1.5 rounded bg-surface-hover text-foreground-muted"},Ks={class:"ml-auto flex items-center gap-1"},Ws=["disabled","title"],Ys={key:1,class:"run-spinner"},Xs=["title"],Qs={class:"h-48 overflow-y-auto bg-background"},Zs={key:0,class:"p-3 font-mono text-xs space-y-0.5"},en={key:0,class:"text-foreground-muted"},tn={key:1,class:"grid grid-cols-1 md:grid-cols-[minmax(0,1fr)_minmax(0,1.3fr)] h-full"},on={class:"flex flex-col min-h-0 border-b md:border-b-0 md:border-r border-border"},sn={class:"h-7 px-2 flex items-center gap-1.5 bg-surface/60 border-b border-border shrink-0"},nn=["disabled"],rn=["value"],an=["disabled"],ln={class:"relative shrink-0"},dn=["disabled","title"],un={key:0,class:"text-[10px] text-foreground-muted"},cn={key:0,class:"px-3 py-3 text-[11px] text-foreground-muted italic"},mn={key:1,class:"max-h-56 overflow-y-auto"},pn=["onClick"],fn={class:"font-mono text-[10px] text-foreground-muted shrink-0"},vn={class:"truncate flex-1 text-foreground"},hn=["title","aria-label","onClick"],gn={class:"border-t border-border px-2 py-1.5 bg-surface/50"},bn=["disabled"],yn={class:"border-b border-border shrink-0"},xn={key:0,class:"ml-1 text-foreground-muted/80 normal-case tracking-normal"},_n={key:0,class:"px-2 py-2 space-y-1"},wn=["onUpdate:modelValue","disabled"],kn=["onUpdate:modelValue","disabled"],Sn=["onClick"],Cn=["disabled"],Tn={class:"h-6 px-3 flex items-center justify-between bg-surface/30 border-b border-border shrink-0"},En={key:0,class:"text-[10px] text-amber-400/80"},On={key:1,class:"text-[10px] text-foreground-muted/70 font-mono"},Rn=["disabled"],jn={class:"flex flex-col min-h-0"},Nn={class:"h-7 px-3 flex items-center justify-between bg-surface/60 border-b border-border shrink-0"},Dn={class:"flex items-center gap-2"},An=["disabled"],Pn={key:1,class:"text-[10px] text-foreground-muted/80 font-mono"},In={class:"flex-1 min-h-0 overflow-y-auto"},Mn={key:1,class:"px-3 py-3 text-xs text-foreground-muted italic"},Vn={key:2,class:"border-t border-border"},Ln={class:"h-6 px-3 flex items-center text-[10px] uppercase tracking-[0.14em] text-foreground-muted/80 bg-surface/30"},$n={class:"px-3 py-2 font-mono text-xs space-y-0.5"},Un={class:"space-y-4"},qn={class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5 flex items-center justify-between"},Bn={key:0,class:"text-[10px] normal-case tracking-normal text-success/80"},zn={class:"grid grid-cols-2 gap-2"},Fn=["onClick"],Hn=["label"],Gn=["value"],Jn={key:0,class:"text-[11px] text-foreground-muted mt-1.5"},Kn={class:"grid grid-cols-2 gap-3"},Wn={class:"border-t border-border pt-4 space-y-2"},Yn={class:"grid grid-cols-2 gap-3"},Xn=["disabled"],Qn={class:"border-t border-border pt-4"},Zn={class:"flex items-start gap-3 cursor-pointer select-none"},er={class:"min-w-0"},tr={class:"text-sm font-medium text-white flex items-center gap-2"},or={class:"border-t border-border pt-4 space-y-2"},sr={class:"text-xs font-medium text-foreground-muted uppercase tracking-wide flex items-center gap-2"},nr={class:"border-t border-border pt-4 space-y-2"},rr={class:"border-t border-border pt-4 space-y-3"},ar={class:"text-xs font-medium text-foreground-muted uppercase tracking-wide flex items-center gap-2"},ir={key:0,class:"text-[10px] normal-case tracking-normal text-foreground-muted"},lr={key:0,class:"text-[11px] text-foreground-muted leading-snug"},dr={key:1,class:"text-[11px] text-foreground-muted leading-snug"},ur={class:"font-mono"},cr={key:2,class:"space-y-1.5"},mr={class:"flex-1 min-w-0 font-mono text-xs text-foreground truncate"},pr={key:0,class:"text-[10px] font-mono text-foreground-muted"},fr=["onClick"],vr={key:3,class:"space-y-2 pt-1"},hr={class:"flex items-center gap-2"},gr={key:0,class:"text-[11px] text-amber-400 leading-snug"},br={class:"font-mono"},yr={class:"font-mono"},xr={key:1,class:"text-[11px] text-red-400 leading-snug"},_r={class:"space-y-2"},wr=["onUpdate:modelValue"],kr=["onUpdate:modelValue"],Sr=["onClick"],Cr={class:"space-y-2"},Tr={class:"text-[10px] text-foreground-muted font-mono"},Er={class:"space-y-3"},Or={key:0,class:"text-xs text-foreground-muted"},Rr={key:0},jr={class:"text-foreground-muted font-mono"},Nr=["onClick"],Dr={class:"flex items-center gap-2 min-w-0"},Ar={class:"text-foreground-muted font-mono"},Pr=["onClick"],Ir={class:"border-t border-border pt-3 space-y-2"},Mr={class:"space-y-2"},Vr={class:"flex items-center gap-2 min-w-0"},Lr={class:"font-mono text-foreground-muted shrink-0"},$r={key:0,class:"px-1.5 py-0.5 rounded text-[10px] bg-success/15 text-success border border-success/30 shrink-0"},Ur=["title"],qr={class:"text-foreground-muted shrink-0"},Br={key:0,class:"shrink-0 flex items-center gap-2"},zr=["disabled","onClick"],Fr={class:"space-y-3 text-xs text-foreground-muted"},Hr={class:"bg-surface border border-border rounded p-3 font-mono text-[12px] text-white overflow-x-auto whitespace-pre"},Gr={class:"space-y-4"},Jr={class:"relative"},Kr={class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5 flex items-center justify-between"},Wr={key:0,class:"text-[10px] normal-case tracking-normal text-success/80"},Yr={class:"grid grid-cols-2 gap-2"},Xr=["onClick"],Qr={class:"grid grid-cols-2 gap-3"},Zr={__name:"Editor",setup(Re){const Dt=xo({loader:()=>To(()=>import("./CodeEditor-Do1Z2Nkh.js"),__vite__mapDeps([0,1,2,3,4])),loadingComponent:{render(){return ge("div",{class:"flex-1 min-h-0 w-full bg-background flex items-start","aria-busy":"true","aria-label":"Loading code editor"},[ge("div",{class:"p-4 space-y-2 w-full font-mono text-xs text-foreground-muted/40"},[ge("div",{class:"h-3 w-1/3 bg-surface-hover rounded animate-pulse"}),ge("div",{class:"h-3 w-2/3 bg-surface-hover rounded animate-pulse"}),ge("div",{class:"h-3 w-1/2 bg-surface-hover rounded animate-pulse"})])])}},delay:0}),U=Eo(),Ke=So(),w=fo(),b=v({settings:!1,envVars:!1,deps:!1,secrets:!1,versions:!1,docs:!1,firstDeploy:!1}),We=v(null),j=v({config:!1,bindings:!1}),Ye=v(null),Xe=v(null),ue=()=>{j.value.config=!1,j.value.bindings=!1},Qe=s=>{const e=!j.value[s];ue(),j.value[s]=e},ae=s=>{ue(),b.value[s]=!0},Ze=s=>{ue(),Ke.push(s)},et=s=>{if(!j.value.config&&!j.value.bindings)return;const e=Ye.value?.contains(s.target),a=Xe.value?.contains(s.target);!e&&!a&&ue()},tt=s=>{s.key==="Escape"&&(j.value.config||j.value.bindings)&&ue()},q=v(!0),B=v("build"),At=E(()=>[{id:"build",label:"Build",icon:Jo,badge:x.value.length||null},{id:"test",label:"Test",icon:kt,badge:ne.value.length||null}]),Pt=s=>{if(!s)return"";const e=/^(python|node)(\d)(\d+)$/.exec(s);if(!e)return s;const a=e[1]==="python"?"Python":"Node.js",t=e[2],r=e[3];return e[1]==="python"?`${a} ${t}.${r}`:`${a} ${t}`},ot=E(()=>F.value.filter(s=>s.key.trim()).length),k=v(""),n=v({name:"",description:"",runtime:"python314",memory_mb:64,cpus:.5,network_mode:"none",max_concurrency:0,concurrency_policy:"queue",auth_mode:"none",rate_limit_per_min:0}),m=v(""),V=v('{"name": "World"}'),z=v("POST"),J=v("/"),D=v([]),K=v(!1),It=["GET","POST","PUT","PATCH","DELETE"],W=v([]),se=v(!1),st=E(()=>D.value.filter(s=>s.name&&s.name.trim()).length),Y=v(!1),ye=v(!1),ce=v(!1),A=v(null),y=v(null),xe=v(0),X=v(""),x=v([]),ne=v([]),_e=v(!1),me=v(!1),we=v(!1),ke=E(()=>m.value?`${window.location.origin}/fn/${m.value}`:""),Mt=async()=>{if(!ke.value)return;await Lo(ke.value)?(_e.value=!0,setTimeout(()=>{_e.value=!1},1500)):w.notify({title:"Copy failed",message:`Could not copy to clipboard. Select the URL manually: +`,jt=[{id:"node-http-hello",category:"Starter",label:"HTTP Hello",description:"Minimal POST /. Echoes a name from the JSON body.",code:Ge,deps:""},{id:"node-discord-webhook",category:"Webhooks",label:"Discord webhook",description:"Ed25519 verify + slash command response.",code:is,deps:ls},{id:"node-jwt-validator",category:"Auth",label:"JWT validator",description:"Verify HS256 token from Authorization header.",code:ds,deps:""},{id:"node-oauth-callback",category:"Auth",label:"OAuth callback",description:"Exchange authorization_code for an access token.",code:us,deps:""},{id:"node-md-to-html",category:"Utility",label:"Markdown → HTML",description:"Render markdown via marked; safe inline HTML.",code:cs,deps:ms},{id:"node-rest-proxy",category:"Utility",label:"REST proxy",description:"Forward to upstream + inject Authorization header.",code:ps,deps:""},{id:"node-email-digest",category:"Scheduled",label:"Email digest",cron:!0,description:"Send a daily summary via SMTP (nodemailer).",code:fs,deps:vs},{id:"node-url-shortener",category:"Utility",label:"URL shortener",description:"POST creates a slug, GET redirects. Uses Orva KV.",code:hs,deps:""},{id:"node-counter-cas",category:"Showcase",label:"v0.6 SDK counter (kv.incr + kv.cas)",description:"Atomic counter and a CAS-retry loop for richer state. Demonstrates v0.6 race-safe primitives and structured logging end-to-end.",code:_s,deps:""},{id:"ts-hello",category:"Starter",label:"TypeScript hello",description:"Typed handler compiled at deploy time via tsc. Outputs to dist/handler.js.",code:gs,deps:bs,extras:{"tsconfig.json":ys},entrypoint:"handler.ts"}],ze={python313:Rt,python314:Rt,node22:jt,node24:jt},Fe={python313:He,python314:He,node22:Ge,node24:Ge},Nt=["Starter","Webhooks","Auth","Utility","Scheduled","Showcase"],ws={class:"flex flex-col h-full"},ks={class:"sr-only"},Ss={class:"flex flex-col sm:flex-row sm:flex-wrap sm:items-center gap-2 pb-3 border-b border-border"},Cs={class:"flex items-center gap-2 sm:mr-auto min-w-0 w-full sm:w-auto"},Ts=["disabled"],Es={class:"text-[11px] text-foreground-muted font-medium tracking-tight shrink-0"},Os={class:"flex flex-wrap items-center gap-2 sm:contents"},Rs=["aria-expanded"],js={key:0,class:"absolute right-0 mt-1 z-30 min-w-[210px] bg-background border border-border rounded-md shadow-xl overflow-hidden",role:"menu"},Ns={key:0,class:"text-[10px] text-foreground-muted tabular-nums"},Ds={key:0,class:"text-[10px] text-foreground-muted tabular-nums"},As={class:"text-[10px] text-foreground-muted tabular-nums"},Ps=["aria-expanded"],Is={key:0,class:"absolute right-0 mt-1 z-30 min-w-[210px] bg-background border border-border rounded-md shadow-xl overflow-hidden",role:"menu"},Vs={key:0,class:"flex items-center gap-2 px-2 py-1.5 mt-2 border border-border bg-surface rounded text-xs"},Ms={class:"font-mono text-white truncate flex-1 min-w-0"},Ls={class:"flex-1 flex flex-col min-h-0 mt-3 bg-background border border-border rounded-lg overflow-hidden shadow-sm"},$s={class:"h-9 border-b border-border flex items-center justify-between px-4 bg-surface shrink-0"},Us={class:"text-xs font-mono text-foreground-muted flex items-center gap-2"},qs={class:"text-white"},Bs={key:0,class:"text-foreground-muted"},zs={class:"text-[10px] text-foreground-muted font-mono"},Fs={class:"mt-3 bg-background border border-border rounded-lg overflow-hidden shrink-0"},Hs={class:"h-9 border-b border-border flex items-center px-2 bg-surface"},Gs=["onClick"],Js={key:0,class:"ml-1 text-[10px] px-1.5 rounded bg-surface-hover text-foreground-muted"},Ks={class:"ml-auto flex items-center gap-1"},Ws=["disabled","title"],Ys={key:1,class:"run-spinner"},Xs=["title"],Qs={class:"h-48 overflow-y-auto bg-background"},Zs={key:0,class:"p-3 font-mono text-xs space-y-0.5"},en={key:0,class:"text-foreground-muted"},tn={key:1,class:"grid grid-cols-1 md:grid-cols-[minmax(0,1fr)_minmax(0,1.3fr)] h-full"},on={class:"flex flex-col min-h-0 border-b md:border-b-0 md:border-r border-border"},sn={class:"h-7 px-2 flex items-center gap-1.5 bg-surface/60 border-b border-border shrink-0"},nn=["disabled"],rn=["value"],an=["disabled"],ln={class:"relative shrink-0"},dn=["disabled","title"],un={key:0,class:"text-[10px] text-foreground-muted"},cn={key:0,class:"px-3 py-3 text-[11px] text-foreground-muted italic"},mn={key:1,class:"max-h-56 overflow-y-auto"},pn=["onClick"],fn={class:"font-mono text-[10px] text-foreground-muted shrink-0"},vn={class:"truncate flex-1 text-foreground"},hn=["title","aria-label","onClick"],gn={class:"border-t border-border px-2 py-1.5 bg-surface/50"},bn=["disabled"],yn={class:"border-b border-border shrink-0"},xn={key:0,class:"ml-1 text-foreground-muted/80 normal-case tracking-normal"},_n={key:0,class:"px-2 py-2 space-y-1"},wn=["onUpdate:modelValue","disabled"],kn=["onUpdate:modelValue","disabled"],Sn=["onClick"],Cn=["disabled"],Tn={class:"h-6 px-3 flex items-center justify-between bg-surface/30 border-b border-border shrink-0"},En={key:0,class:"text-[10px] text-amber-400/80"},On={key:1,class:"text-[10px] text-foreground-muted/70 font-mono"},Rn=["disabled"],jn={class:"flex flex-col min-h-0"},Nn={class:"h-7 px-3 flex items-center justify-between bg-surface/60 border-b border-border shrink-0"},Dn={class:"flex items-center gap-2"},An=["disabled"],Pn={key:1,class:"text-[10px] text-foreground-muted/80 font-mono"},In={class:"flex-1 min-h-0 overflow-y-auto"},Vn={key:1,class:"px-3 py-3 text-xs text-foreground-muted italic"},Mn={key:2,class:"border-t border-border"},Ln={class:"h-6 px-3 flex items-center text-[10px] uppercase tracking-[0.14em] text-foreground-muted/80 bg-surface/30"},$n={class:"px-3 py-2 font-mono text-xs space-y-0.5"},Un={class:"space-y-4"},qn={class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5 flex items-center justify-between"},Bn={key:0,class:"text-[10px] normal-case tracking-normal text-success/80"},zn={class:"grid grid-cols-2 gap-2"},Fn=["onClick"],Hn=["label"],Gn=["value"],Jn={key:0,class:"text-[11px] text-foreground-muted mt-1.5"},Kn={class:"grid grid-cols-2 gap-3"},Wn={class:"border-t border-border pt-4 space-y-2"},Yn={class:"grid grid-cols-2 gap-3"},Xn=["disabled"],Qn={class:"border-t border-border pt-4"},Zn={class:"flex items-start gap-3 cursor-pointer select-none"},er={class:"min-w-0"},tr={class:"text-sm font-medium text-white flex items-center gap-2"},or={class:"border-t border-border pt-4 space-y-2"},sr={class:"text-xs font-medium text-foreground-muted uppercase tracking-wide flex items-center gap-2"},nr={class:"border-t border-border pt-4 space-y-2"},rr={class:"border-t border-border pt-4 space-y-3"},ar={class:"text-xs font-medium text-foreground-muted uppercase tracking-wide flex items-center gap-2"},ir={key:0,class:"text-[10px] normal-case tracking-normal text-foreground-muted"},lr={key:0,class:"text-[11px] text-foreground-muted leading-snug"},dr={key:1,class:"text-[11px] text-foreground-muted leading-snug"},ur={class:"font-mono"},cr={key:2,class:"space-y-1.5"},mr={class:"flex-1 min-w-0 font-mono text-xs text-foreground truncate"},pr={key:0,class:"text-[10px] font-mono text-foreground-muted"},fr=["onClick"],vr={key:3,class:"space-y-2 pt-1"},hr={class:"flex items-center gap-2"},gr={key:0,class:"text-[11px] text-amber-400 leading-snug"},br={class:"font-mono"},yr={class:"font-mono"},xr={key:1,class:"text-[11px] text-red-400 leading-snug"},_r={class:"space-y-2"},wr=["onUpdate:modelValue"],kr=["onUpdate:modelValue"],Sr=["onClick"],Cr={class:"space-y-2"},Tr={class:"text-[10px] text-foreground-muted font-mono"},Er={class:"space-y-3"},Or={key:0,class:"text-xs text-foreground-muted"},Rr={key:0},jr={class:"text-foreground-muted font-mono"},Nr=["onClick"],Dr={class:"flex items-center gap-2 min-w-0"},Ar={class:"text-foreground-muted font-mono"},Pr=["onClick"],Ir={class:"border-t border-border pt-3 space-y-2"},Vr={class:"space-y-2"},Mr={class:"flex items-center gap-2 min-w-0"},Lr={class:"font-mono text-foreground-muted shrink-0"},$r={key:0,class:"px-1.5 py-0.5 rounded text-[10px] bg-success/15 text-success border border-success/30 shrink-0"},Ur=["title"],qr={class:"text-foreground-muted shrink-0"},Br={key:0,class:"shrink-0 flex items-center gap-2"},zr=["disabled","onClick"],Fr={class:"space-y-3 text-xs text-foreground-muted"},Hr={class:"bg-surface border border-border rounded p-3 font-mono text-[12px] text-white overflow-x-auto whitespace-pre"},Gr={class:"space-y-4"},Jr={class:"relative"},Kr={class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5 flex items-center justify-between"},Wr={key:0,class:"text-[10px] normal-case tracking-normal text-success/80"},Yr={class:"grid grid-cols-2 gap-2"},Xr=["onClick"],Qr={class:"grid grid-cols-2 gap-3"},Zr={__name:"Editor",setup(Re){const Dt=xo({loader:()=>To(()=>import("./CodeEditor-se_FXC0j.js"),__vite__mapDeps([0,1,2,3,4])),loadingComponent:{render(){return ge("div",{class:"flex-1 min-h-0 w-full bg-background flex items-start","aria-busy":"true","aria-label":"Loading code editor"},[ge("div",{class:"p-4 space-y-2 w-full font-mono text-xs text-foreground-muted/40"},[ge("div",{class:"h-3 w-1/3 bg-surface-hover rounded animate-pulse"}),ge("div",{class:"h-3 w-2/3 bg-surface-hover rounded animate-pulse"}),ge("div",{class:"h-3 w-1/2 bg-surface-hover rounded animate-pulse"})])])}},delay:0}),U=Eo(),Ke=So(),w=fo(),b=v({settings:!1,envVars:!1,deps:!1,secrets:!1,versions:!1,docs:!1,firstDeploy:!1}),We=v(null),j=v({config:!1,bindings:!1}),Ye=v(null),Xe=v(null),ue=()=>{j.value.config=!1,j.value.bindings=!1},Qe=s=>{const e=!j.value[s];ue(),j.value[s]=e},ae=s=>{ue(),b.value[s]=!0},Ze=s=>{ue(),Ke.push(s)},et=s=>{if(!j.value.config&&!j.value.bindings)return;const e=Ye.value?.contains(s.target),a=Xe.value?.contains(s.target);!e&&!a&&ue()},tt=s=>{s.key==="Escape"&&(j.value.config||j.value.bindings)&&ue()},q=v(!0),B=v("build"),At=E(()=>[{id:"build",label:"Build",icon:Jo,badge:x.value.length||null},{id:"test",label:"Test",icon:kt,badge:ne.value.length||null}]),Pt=s=>{if(!s)return"";const e=/^(python|node)(\d)(\d+)$/.exec(s);if(!e)return s;const a=e[1]==="python"?"Python":"Node.js",t=e[2],r=e[3];return e[1]==="python"?`${a} ${t}.${r}`:`${a} ${t}`},ot=E(()=>F.value.filter(s=>s.key.trim()).length),k=v(""),n=v({name:"",description:"",runtime:"python314",memory_mb:64,cpus:.5,network_mode:"none",max_concurrency:0,concurrency_policy:"queue",auth_mode:"none",rate_limit_per_min:0}),m=v(""),M=v('{"name": "World"}'),z=v("POST"),J=v("/"),D=v([]),K=v(!1),It=["GET","POST","PUT","PATCH","DELETE"],W=v([]),se=v(!1),st=E(()=>D.value.filter(s=>s.name&&s.name.trim()).length),Y=v(!1),ye=v(!1),ce=v(!1),A=v(null),y=v(null),xe=v(0),X=v(""),x=v([]),ne=v([]),_e=v(!1),me=v(!1),we=v(!1),ke=E(()=>m.value?`${window.location.origin}/fn/${m.value}`:""),Vt=async()=>{if(!ke.value)return;await Mo(ke.value)?(_e.value=!0,setTimeout(()=>{_e.value=!1},1500)):w.notify({title:"Copy failed",message:`Could not copy to clipboard. Select the URL manually: -`+ke.value})},F=v([{key:"",value:""}]),Q=v(""),Z=v(""),ie=v([]),je=v([]),P=v({name:"",value:""}),Ne=v([]),De=v(!1),I=v(""),ee=v({path:"",methods:"*"}),L=v(null),nt=E(()=>m.value?Ne.value.filter(s=>s.function_id===m.value):[]),H=v([]),Ae=E(()=>je.value.length+H.value.length),R=E(()=>!!U.params.name),Vt=E(()=>R.value||ce.value),$=E(()=>Vt.value&&!Y.value),rt=[{id:"python314",label:"Python 3.14"},{id:"python313",label:"Python 3.13"},{id:"node24",label:"Node.js 24 (LTS)"},{id:"node22",label:"Node.js 22 (LTS)"}],Se=s=>s==="python313"||s==="python314",at=s=>s==="node22"||s==="node24",it=E(()=>Se(n.value.runtime)?"handler.py":(at(n.value.runtime),"handler.js")),Lt=E(()=>Se(n.value.runtime)?`def handler(event): +`+ke.value})},F=v([{key:"",value:""}]),Q=v(""),Z=v(""),ie=v([]),je=v([]),P=v({name:"",value:""}),Ne=v([]),De=v(!1),I=v(""),ee=v({path:"",methods:"*"}),L=v(null),nt=E(()=>m.value?Ne.value.filter(s=>s.function_id===m.value):[]),H=v([]),Ae=E(()=>je.value.length+H.value.length),R=E(()=>!!U.params.name),Mt=E(()=>R.value||ce.value),$=E(()=>Mt.value&&!Y.value),rt=[{id:"python314",label:"Python 3.14"},{id:"python313",label:"Python 3.13"},{id:"node24",label:"Node.js 24 (LTS)"},{id:"node22",label:"Node.js 22 (LTS)"}],Se=s=>s==="python313"||s==="python314",at=s=>s==="node22"||s==="node24",it=E(()=>Se(n.value.runtime)?"handler.py":(at(n.value.runtime),"handler.js")),Lt=E(()=>Se(n.value.runtime)?`def handler(event): return { "statusCode": 200, "body": "ok" }`:`exports.handler = async (event) => ({ statusCode: 200, body: 'ok' -});`),$t=E(()=>Se(n.value.runtime)?"requirements.txt":"package.json"),Pe=s=>{n.value.runtime=s,!R.value&&(!k.value||Object.values(Fe).includes(k.value))&&(k.value=Fe[s]||""),R.value||(Z.value="",Q.value="")},lt=s=>{pe.value=!0,Pe(s)},pe=v(!1),fe=v(!1),Ut=s=>{if(!s||s.length<10)return null;const e=[/\bdef\s+handler\s*\(/,/^import\s+\w/m,/^from\s+[\w.]+\s+import/m,/:\s*$/m,/\bprint\s*\(/,/\b(True|False|None)\b/,/\belif\b/],a=[/=>\s*[{(]/,/\bconst\s+\w/,/\blet\s+\w/,/module\.exports\b/,/\brequire\s*\(/,/\bexports\.\w/,/\basync\s+function\b/,/\bawait\s+\w/,/;\s*$/m],t=e.filter(c=>c.test(s)).length,r=a.filter(c=>c.test(s)).length;return t>=r+2?"python":r>=t+2?"node":null};let Ie=null;const qt=s=>{pe.value||R.value||(Ie&&clearTimeout(Ie),Ie=setTimeout(()=>{const e=Ut(s);if(!e)return;const a=Se(n.value.runtime),t=at(n.value.runtime);e==="python"&&!a?(n.value.runtime="python314",fe.value=!0):e==="node"&&!t&&(n.value.runtime="node24",fe.value=!0)},400))};he(k,s=>qt(s));const Bt=()=>{const e=(ze[n.value.runtime]||[]).find(a=>a.id===Z.value);e&&(k.value=e.code,Q.value=e.deps||"",e.description&&!n.value.description&&(n.value.description=e.description))},zt=E(()=>{const s=ze[n.value.runtime]||[],e=new Map;for(const t of s){const r=t.category||"Other";e.has(r)||e.set(r,[]),e.get(r).push(t)}const a=Nt.filter(t=>e.has(t)).map(t=>({label:t,items:e.get(t)}));for(const[t,r]of e)Nt.includes(t)||a.push({label:t,items:r});return a}),dt=E(()=>(ze[n.value.runtime]||[]).find(a=>a.id===Z.value)?.description||""),Ft=()=>F.value.push({key:"",value:""}),Ht=s=>F.value.splice(s,1),Me=async()=>{if(m.value){De.value=!0,I.value="";try{const s=await Mo();Ne.value=s.data?.routes||[]}catch(s){I.value=s.response?.data?.error||s.message||"Failed to load routes"}finally{De.value=!1}}};he(()=>ee.value.path,s=>{const e=(s||"").trim();if(!e){L.value=null;return}const a=Ne.value.find(t=>t.path===e);a&&a.function_id!==m.value?L.value={path:e,currentFunctionId:a.function_id}:L.value=null});const Gt=async()=>{if(!m.value){I.value="Save the function first. Routes need a target function id.";return}const s=(ee.value.path||"").trim(),e=(ee.value.methods||"*").trim()||"*";if(!s){I.value="Path is required (must start with /).";return}if(!s.startsWith("/")){I.value="Path must start with /.";return}if(!(L.value&&L.value.path===s&&!await w.ask({title:"Remap existing route?",message:`${s} currently points at function ${L.value.currentFunctionId.slice(0,8)}…. Saving will remap it to this function.`,confirmLabel:"Remap",cancelLabel:"Keep existing",danger:!0}))){I.value="";try{await Po(s,m.value,e),ee.value={path:"",methods:"*"},L.value=null,await Me()}catch(a){I.value=a.response?.data?.error||a.message||"Failed to save route"}}},Jt=async s=>{if(await w.ask({title:"Remove custom route?",message:`${s} will stop dispatching to this function. The function itself stays untouched and is still reachable at /fn/.`,confirmLabel:"Remove route",cancelLabel:"Keep",danger:!0})){I.value="";try{await Ao(s),await Me()}catch(a){I.value=a.response?.data?.error||a.message||"Failed to delete route"}}};he(()=>b.value.settings,s=>{s&&Me()});const Kt=()=>{m.value="",n.value={name:"",description:"",runtime:"python314",memory_mb:64,cpus:.5,network_mode:"none",max_concurrency:0,concurrency_policy:"queue",auth_mode:"none",rate_limit_per_min:0},F.value=[],k.value="",Q.value="",x.value=[],ne.value=[],A.value=null,y.value=null,xe.value=0,X.value="",ce.value=!1,ie.value=[],W.value=[],z.value="POST",J.value="/",D.value=[],V.value='{"name": "World"}',K.value=!1,fe.value=!1,pe.value=!1,Z.value=""};he(()=>U.params.name,async()=>{if(Kt(),U.params.name)try{const e=((await T.get("/functions")).data.functions||[]).find(a=>a.name===U.params.name);if(!e)throw new Error("Function not found");m.value=e.id,n.value.name=e.name,n.value.description=e.description||"",n.value.runtime=e.runtime,n.value.memory_mb=e.memory_mb,n.value.cpus=e.cpus,n.value.network_mode=e.network_mode||"none",n.value.max_concurrency=e.max_concurrency||0,n.value.concurrency_policy=e.concurrency_policy||"queue",n.value.auth_mode=e.auth_mode||"none",n.value.rate_limit_per_min=e.rate_limit_per_min||0,e.env_vars&&Object.keys(e.env_vars).length>0&&(F.value=Object.entries(e.env_vars).map(([a,t])=>({key:a,value:t}))),await Ve(e),await Ee(),await Le(e),Wt()}catch(s){console.error("Failed to load function",s),y.value="Failed to load function: "+(s.response?.data?.error?.message||s.message)}else Pe("python314"),n.value.name=Ot()},{immediate:!0});const Ve=async s=>{if(s){try{const e=await T.get(`/functions/${s.id}/source`);if(e.data.code){k.value=e.data.code,Q.value=e.data.dependencies||"";return}}catch{}k.value=Fe[s.runtime]||"",Q.value=""}},ut=async()=>{if(m.value)try{const e=((await T.get("/functions")).data.functions||[]).find(a=>a.id===m.value);if(!e)return;await Ve(e),await Le(e)}catch{}},ct=()=>{Y.value||pt()};vo(()=>{window.addEventListener("focus",ut),document.addEventListener("mousedown",et),document.addEventListener("keydown",tt),window.addEventListener("orva:deploy",ct)}),gt(()=>{window.removeEventListener("focus",ut),document.removeEventListener("mousedown",et),document.removeEventListener("keydown",tt),window.removeEventListener("orva:deploy",ct)});const Wt=()=>{const s=U.query.prefill;if(s)try{const e=atob(String(s)),a=JSON.parse(e);a.method&&(z.value=String(a.method).toUpperCase()),a.path&&(J.value=String(a.path)),a.headers&&typeof a.headers=="object"&&(D.value=Object.entries(a.headers).map(([t,r])=>({name:t,value:String(r)})),D.value.length&&(K.value=!0)),a.body!==void 0&&(V.value=typeof a.body=="string"?a.body:JSON.stringify(a.body,null,2)),B.value="test",q.value=!0,Ke.replace({query:{...U.query,prefill:void 0}})}catch{}},mt=()=>{R.value||m.value||(n.value.name=Ot())},ve=E(()=>ie.value.find(e=>e.is_active)?.deployment_id||null),Le=async s=>{try{const a=(await T.get(`/functions/${s.id}/deployments?limit=50`)).data.deployments||[],t=new Set;ie.value=a.filter(r=>r.status==="succeeded"&&r.code_hash).filter(r=>t.has(r.code_hash)?!1:(t.add(r.code_hash),!0)).map(r=>({deployment_id:r.id,version:r.version,code_hash:r.code_hash,short_hash:r.code_hash.slice(0,12),created_at:r.finished_at||r.submitted_at,is_active:r.code_hash===s.code_hash}))}catch(e){console.warn("failed to load versions",e)}};gt(()=>{if(te){try{te.close()}catch{}te=null}});const pt=async()=>{if(!k.value){w.notify({title:"Missing code",message:"Write a handler before deploying."});return}if(!R.value&&!m.value&&!n.value.name.trim()){b.value.firstDeploy=!0,setTimeout(()=>We.value?.focus(),50);return}await vt()},ft=async()=>{n.value.name.trim()&&(b.value.firstDeploy=!1,await vt())},vt=async()=>{if(!n.value.name||!k.value){w.notify({title:"Missing fields",message:"Please provide a function name and code."});return}B.value="build",q.value=!0,Y.value=!0,A.value=null,y.value=null,x.value=["Starting deployment..."];try{const s={};for(const{key:t,value:r}of F.value)t.trim()&&(s[t.trim()]=r);if(m.value)await T.put(`/functions/${m.value}`,{description:n.value.description||"",memory_mb:n.value.memory_mb,cpus:n.value.cpus,env_vars:s,network_mode:n.value.network_mode,max_concurrency:n.value.max_concurrency||0,concurrency_policy:n.value.concurrency_policy||"queue",auth_mode:n.value.auth_mode||"none",rate_limit_per_min:n.value.rate_limit_per_min||0}),x.value.push("Updated function config");else try{const t=await T.post("/functions",{name:n.value.name,description:n.value.description||"",runtime:n.value.runtime,memory_mb:n.value.memory_mb,cpus:n.value.cpus,env_vars:s,network_mode:n.value.network_mode,max_concurrency:n.value.max_concurrency||0,concurrency_policy:n.value.concurrency_policy||"queue",auth_mode:n.value.auth_mode||"none",rate_limit_per_min:n.value.rate_limit_per_min||0});m.value=t.data.id,x.value.push(`Created function: ${m.value}`)}catch(t){if(t.response?.status===409){const c=((await T.get("/functions")).data.functions||[]).find(N=>N.name===n.value.name);if(c)m.value=c.id,x.value.push(`Function already exists: ${m.value}`);else throw new Error("Function name conflict but not found in list")}else throw t}await co(),x.value.push("Submitting build...");const e=await T.post(`/functions/${m.value}/deploy-inline`,{code:k.value,filename:it.value,dependencies:Q.value||""}),a=e.data.deployment_id;if(!a){x.value.push(`Deployed! Hash: ${e.data.code_hash||"unknown"}`),ce.value=!0,Y.value=!1;return}x.value.push(`Build queued (${a})`),await Yt(a)}catch(s){y.value=s.response?.data?.error?.message||s.message||"Deployment failed",x.value.push(`Error: ${y.value}`),Y.value=!1}};let te=null;const Yt=s=>new Promise(e=>{if(te){try{te.close()}catch{}te=null}const a=new EventSource(`/api/v1/deployments/${s}/stream`);te=a;let t=!1;const r=(c,N)=>{if(!t){t=!0;try{a.close()}catch{}if(te=null,Y.value=!1,c)ce.value=!0,x.value.push(`✓ Build succeeded in ${N?.duration_ms??"?"}ms`),B.value="test";else{const G=N?.error_message||"build failed (see logs)";y.value=G,x.value.push(`✗ Build failed: ${G}`)}e()}};a.addEventListener("log",c=>{try{const N=JSON.parse(c.data),G=`[${N.stream||"log"}] ${N.line}`;x.value.push(G),x.value.length>500&&x.value.splice(0,x.value.length-500)}catch{}}),a.addEventListener("succeeded",c=>{try{r(!0,JSON.parse(c.data))}catch{r(!0)}}),a.addEventListener("failed",c=>{try{r(!1,JSON.parse(c.data))}catch{r(!1)}}),a.onerror=()=>{t||a.readyState===EventSource.CLOSED&&fetch(`/api/v1/deployments/${s}`,{credentials:"include"}).then(c=>c.ok?c.json():null).then(c=>{if(c&&c.status==="succeeded")return r(!0,c);if(c&&c.status==="failed")return r(!1,c);r(!1,{error_message:"stream closed before terminal state"})}).catch(()=>r(!1,{error_message:"stream closed; deployment status unknown"}))}}),ht=()=>{const s={};for(const e of D.value){const a=(e.name||"").trim();a&&(s[a]=e.value??"")}return s},Xt=(s,e,a)=>{const t=(e||"POST").toUpperCase();return t==="GET"||t==="HEAD"||!a||Object.keys(s).some(c=>c.toLowerCase()==="content-type")||(s["Content-Type"]="application/json"),s},Qt=async()=>{if(!m.value){y.value="No function deployed yet";return}B.value="test",q.value=!0,ye.value=!0,A.value=null,y.value=null,ne.value=[],me.value=!1;try{const s=Xt(ht(),z.value,V.value);s["X-Orva-API-Key"]=s["X-Orva-API-Key"]||_o();const e=await wo(m.value,{method:z.value,path:J.value||"/",headers:s,body:V.value||""}),a=typeof e.data=="string"?e.data:JSON.stringify(e.data);X.value=`${e.status}`,xe.value=e.headers?.["x-orva-duration-ms"]||e.headers?.["X-Orva-Duration-MS"]||"?";try{A.value=JSON.stringify(JSON.parse(a),null,2)}catch{A.value=a}typeof e.status=="number"&&e.status>=500&&(me.value=!0)}catch(s){if(s.response){X.value=`${s.response.status}`;const e=s.response.data;y.value=typeof e=="string"?e:JSON.stringify(e),s.response.status>=500&&(me.value=!0)}else y.value=s.message||"Invocation failed",X.value="Error",me.value=!0}finally{ye.value=!1}},Zt=async()=>{if(!we.value){we.value=!0;try{const s=(ne.value||[]).join(` -`),e={};for(const c of D.value||[])c?.name&&c.name.trim()&&(e[c.name.trim()]=c.value||"");const a={method:z.value||"POST",path:J.value||"/",headers:e,body:V.value||""},t=/^\d+$/.test(X.value)?Number(X.value):X.value;await $o({source:k.value||"",runtime:n.value.runtime||"",stderr:s,requestPreview:a,errorMessage:y.value||"",statusCode:t||""})?w.notify({title:"Prompt copied",message:"Paste into ChatGPT, Claude, or your AI tool of choice."}):w.notify({title:"Copy failed",message:"Could not write to the clipboard. Try again, or copy the stderr by hand.",danger:!0})}finally{we.value=!1}}},eo=()=>{D.value.push({name:"",value:""}),K.value=!0},to=s=>{D.value.splice(s,1)},Ce=async()=>{if(m.value)try{const s=await Co(m.value);W.value=s.data?.fixtures||[]}catch{W.value=[]}},oo=async()=>{se.value=!se.value,se.value&&await Ce()},so=s=>{z.value=s.method||"POST",J.value=s.path||"/",D.value=Object.entries(s.headers||{}).map(([e,a])=>({name:e,value:a})),D.value.length&&(K.value=!0),V.value=s.body||"",se.value=!1},no=async s=>{if(await w.ask({title:`Delete fixture "${s.name}"?`,message:"This only removes the saved request preset. Function code and execution history are untouched.",confirmLabel:"Delete",danger:!0}))try{await Do(m.value,s.name),await Ce()}catch(a){w.notify({title:"Delete failed",message:a.response?.data?.error?.message||a.message,danger:!0})}},ro=async()=>{if(!m.value)return;const e=(await w.prompt({title:"Save fixture",message:"Give this request a name so you can replay or share it later.",placeholder:"e.g. happy-path, signed-stripe, empty-body",confirmLabel:"Save"})||"").trim();if(!e)return;const a=ht(),t={name:e,method:z.value,path:J.value||"/",headers:a,body:V.value||""};try{await ko(m.value,e,t),se.value=!1,await Ce()}catch(r){w.notify({title:"Save failed",message:r.response?.data?.error?.message||r.message,danger:!0})}};he(m,async s=>{s?await Ce():W.value=[]});const Te=v(!1),ao=async s=>{if(!m.value||!s?.deployment_id||Te.value)return;let e=`Code hash ${s.short_hash}. Your current version stays in the history.`;try{const[t,r]=await Promise.all([T.get(`/deployments/${s.deployment_id}`),T.get("/functions")]),c=t.data?.snapshot,N=(r.data.functions||[]).find(G=>G.id===m.value);if(c&&N){const G=Uo(N,c);G.length?e=`Rolling back to v${s.version} (code ${s.short_hash}) will also change: +});`),$t=E(()=>Se(n.value.runtime)?"requirements.txt":"package.json"),Pe=s=>{n.value.runtime=s,!R.value&&(!k.value||Object.values(Fe).includes(k.value))&&(k.value=Fe[s]||""),R.value||(Z.value="",Q.value="")},lt=s=>{pe.value=!0,Pe(s)},pe=v(!1),fe=v(!1),Ut=s=>{if(!s||s.length<10)return null;const e=[/\bdef\s+handler\s*\(/,/^import\s+\w/m,/^from\s+[\w.]+\s+import/m,/:\s*$/m,/\bprint\s*\(/,/\b(True|False|None)\b/,/\belif\b/],a=[/=>\s*[{(]/,/\bconst\s+\w/,/\blet\s+\w/,/module\.exports\b/,/\brequire\s*\(/,/\bexports\.\w/,/\basync\s+function\b/,/\bawait\s+\w/,/;\s*$/m],t=e.filter(c=>c.test(s)).length,r=a.filter(c=>c.test(s)).length;return t>=r+2?"python":r>=t+2?"node":null};let Ie=null;const qt=s=>{pe.value||R.value||(Ie&&clearTimeout(Ie),Ie=setTimeout(()=>{const e=Ut(s);if(!e)return;const a=Se(n.value.runtime),t=at(n.value.runtime);e==="python"&&!a?(n.value.runtime="python314",fe.value=!0):e==="node"&&!t&&(n.value.runtime="node24",fe.value=!0)},400))};he(k,s=>qt(s));const Bt=()=>{const e=(ze[n.value.runtime]||[]).find(a=>a.id===Z.value);e&&(k.value=e.code,Q.value=e.deps||"",e.description&&!n.value.description&&(n.value.description=e.description))},zt=E(()=>{const s=ze[n.value.runtime]||[],e=new Map;for(const t of s){const r=t.category||"Other";e.has(r)||e.set(r,[]),e.get(r).push(t)}const a=Nt.filter(t=>e.has(t)).map(t=>({label:t,items:e.get(t)}));for(const[t,r]of e)Nt.includes(t)||a.push({label:t,items:r});return a}),dt=E(()=>(ze[n.value.runtime]||[]).find(a=>a.id===Z.value)?.description||""),Ft=()=>F.value.push({key:"",value:""}),Ht=s=>F.value.splice(s,1),Ve=async()=>{if(m.value){De.value=!0,I.value="";try{const s=await Vo();Ne.value=s.data?.routes||[]}catch(s){I.value=s.response?.data?.error||s.message||"Failed to load routes"}finally{De.value=!1}}};he(()=>ee.value.path,s=>{const e=(s||"").trim();if(!e){L.value=null;return}const a=Ne.value.find(t=>t.path===e);a&&a.function_id!==m.value?L.value={path:e,currentFunctionId:a.function_id}:L.value=null});const Gt=async()=>{if(!m.value){I.value="Save the function first. Routes need a target function id.";return}const s=(ee.value.path||"").trim(),e=(ee.value.methods||"*").trim()||"*";if(!s){I.value="Path is required (must start with /).";return}if(!s.startsWith("/")){I.value="Path must start with /.";return}if(!(L.value&&L.value.path===s&&!await w.ask({title:"Remap existing route?",message:`${s} currently points at function ${L.value.currentFunctionId.slice(0,8)}…. Saving will remap it to this function.`,confirmLabel:"Remap",cancelLabel:"Keep existing",danger:!0}))){I.value="";try{await Po(s,m.value,e),ee.value={path:"",methods:"*"},L.value=null,await Ve()}catch(a){I.value=a.response?.data?.error||a.message||"Failed to save route"}}},Jt=async s=>{if(await w.ask({title:"Remove custom route?",message:`${s} will stop dispatching to this function. The function itself stays untouched and is still reachable at /fn/.`,confirmLabel:"Remove route",cancelLabel:"Keep",danger:!0})){I.value="";try{await Ao(s),await Ve()}catch(a){I.value=a.response?.data?.error||a.message||"Failed to delete route"}}};he(()=>b.value.settings,s=>{s&&Ve()});const Kt=()=>{m.value="",n.value={name:"",description:"",runtime:"python314",memory_mb:64,cpus:.5,network_mode:"none",max_concurrency:0,concurrency_policy:"queue",auth_mode:"none",rate_limit_per_min:0},F.value=[],k.value="",Q.value="",x.value=[],ne.value=[],A.value=null,y.value=null,xe.value=0,X.value="",ce.value=!1,ie.value=[],W.value=[],z.value="POST",J.value="/",D.value=[],M.value='{"name": "World"}',K.value=!1,fe.value=!1,pe.value=!1,Z.value=""};he(()=>U.params.name,async()=>{if(Kt(),U.params.name)try{const e=((await T.get("/functions")).data.functions||[]).find(a=>a.name===U.params.name);if(!e)throw new Error("Function not found");m.value=e.id,n.value.name=e.name,n.value.description=e.description||"",n.value.runtime=e.runtime,n.value.memory_mb=e.memory_mb,n.value.cpus=e.cpus,n.value.network_mode=e.network_mode||"none",n.value.max_concurrency=e.max_concurrency||0,n.value.concurrency_policy=e.concurrency_policy||"queue",n.value.auth_mode=e.auth_mode||"none",n.value.rate_limit_per_min=e.rate_limit_per_min||0,e.env_vars&&Object.keys(e.env_vars).length>0&&(F.value=Object.entries(e.env_vars).map(([a,t])=>({key:a,value:t}))),await Me(e),await Ee(),await Le(e),Wt()}catch(s){console.error("Failed to load function",s),y.value="Failed to load function: "+(s.response?.data?.error?.message||s.message)}else Pe("python314"),n.value.name=Ot()},{immediate:!0});const Me=async s=>{if(s){try{const e=await T.get(`/functions/${s.id}/source`);if(e.data.code){k.value=e.data.code,Q.value=e.data.dependencies||"";return}}catch{}k.value=Fe[s.runtime]||"",Q.value=""}},ut=async()=>{if(m.value)try{const e=((await T.get("/functions")).data.functions||[]).find(a=>a.id===m.value);if(!e)return;await Me(e),await Le(e)}catch{}},ct=()=>{Y.value||pt()};vo(()=>{window.addEventListener("focus",ut),document.addEventListener("mousedown",et),document.addEventListener("keydown",tt),window.addEventListener("orva:deploy",ct)}),gt(()=>{window.removeEventListener("focus",ut),document.removeEventListener("mousedown",et),document.removeEventListener("keydown",tt),window.removeEventListener("orva:deploy",ct)});const Wt=()=>{const s=U.query.prefill;if(s)try{const e=atob(String(s)),a=JSON.parse(e);a.method&&(z.value=String(a.method).toUpperCase()),a.path&&(J.value=String(a.path)),a.headers&&typeof a.headers=="object"&&(D.value=Object.entries(a.headers).map(([t,r])=>({name:t,value:String(r)})),D.value.length&&(K.value=!0)),a.body!==void 0&&(M.value=typeof a.body=="string"?a.body:JSON.stringify(a.body,null,2)),B.value="test",q.value=!0,Ke.replace({query:{...U.query,prefill:void 0}})}catch{}},mt=()=>{R.value||m.value||(n.value.name=Ot())},ve=E(()=>ie.value.find(e=>e.is_active)?.deployment_id||null),Le=async s=>{try{const a=(await T.get(`/functions/${s.id}/deployments?limit=50`)).data.deployments||[],t=new Set;ie.value=a.filter(r=>r.status==="succeeded"&&r.code_hash).filter(r=>t.has(r.code_hash)?!1:(t.add(r.code_hash),!0)).map(r=>({deployment_id:r.id,version:r.version,code_hash:r.code_hash,short_hash:r.code_hash.slice(0,12),created_at:r.finished_at||r.submitted_at,is_active:r.code_hash===s.code_hash}))}catch(e){console.warn("failed to load versions",e)}};gt(()=>{if(te){try{te.close()}catch{}te=null}});const pt=async()=>{if(!k.value){w.notify({title:"Missing code",message:"Write a handler before deploying."});return}if(!R.value&&!m.value&&!n.value.name.trim()){b.value.firstDeploy=!0,setTimeout(()=>We.value?.focus(),50);return}await vt()},ft=async()=>{n.value.name.trim()&&(b.value.firstDeploy=!1,await vt())},vt=async()=>{if(!n.value.name||!k.value){w.notify({title:"Missing fields",message:"Please provide a function name and code."});return}B.value="build",q.value=!0,Y.value=!0,A.value=null,y.value=null,x.value=["Starting deployment..."];try{const s={};for(const{key:t,value:r}of F.value)t.trim()&&(s[t.trim()]=r);if(m.value)await T.put(`/functions/${m.value}`,{description:n.value.description||"",memory_mb:n.value.memory_mb,cpus:n.value.cpus,env_vars:s,network_mode:n.value.network_mode,max_concurrency:n.value.max_concurrency||0,concurrency_policy:n.value.concurrency_policy||"queue",auth_mode:n.value.auth_mode||"none",rate_limit_per_min:n.value.rate_limit_per_min||0}),x.value.push("Updated function config");else try{const t=await T.post("/functions",{name:n.value.name,description:n.value.description||"",runtime:n.value.runtime,memory_mb:n.value.memory_mb,cpus:n.value.cpus,env_vars:s,network_mode:n.value.network_mode,max_concurrency:n.value.max_concurrency||0,concurrency_policy:n.value.concurrency_policy||"queue",auth_mode:n.value.auth_mode||"none",rate_limit_per_min:n.value.rate_limit_per_min||0});m.value=t.data.id,x.value.push(`Created function: ${m.value}`)}catch(t){if(t.response?.status===409){const c=((await T.get("/functions")).data.functions||[]).find(N=>N.name===n.value.name);if(c)m.value=c.id,x.value.push(`Function already exists: ${m.value}`);else throw new Error("Function name conflict but not found in list")}else throw t}await co(),x.value.push("Submitting build...");const e=await T.post(`/functions/${m.value}/deploy-inline`,{code:k.value,filename:it.value,dependencies:Q.value||""}),a=e.data.deployment_id;if(!a){x.value.push(`Deployed! Hash: ${e.data.code_hash||"unknown"}`),ce.value=!0,Y.value=!1;return}x.value.push(`Build queued (${a})`),await Yt(a)}catch(s){y.value=s.response?.data?.error?.message||s.message||"Deployment failed",x.value.push(`Error: ${y.value}`),Y.value=!1}};let te=null;const Yt=s=>new Promise(e=>{if(te){try{te.close()}catch{}te=null}const a=new EventSource(`/api/v1/deployments/${s}/stream`);te=a;let t=!1;const r=(c,N)=>{if(!t){t=!0;try{a.close()}catch{}if(te=null,Y.value=!1,c)ce.value=!0,x.value.push(`✓ Build succeeded in ${N?.duration_ms??"?"}ms`),B.value="test";else{const G=N?.error_message||"build failed (see logs)";y.value=G,x.value.push(`✗ Build failed: ${G}`)}e()}};a.addEventListener("log",c=>{try{const N=JSON.parse(c.data),G=`[${N.stream||"log"}] ${N.line}`;x.value.push(G),x.value.length>500&&x.value.splice(0,x.value.length-500)}catch{}}),a.addEventListener("succeeded",c=>{try{r(!0,JSON.parse(c.data))}catch{r(!0)}}),a.addEventListener("failed",c=>{try{r(!1,JSON.parse(c.data))}catch{r(!1)}}),a.onerror=()=>{t||a.readyState===EventSource.CLOSED&&fetch(`/api/v1/deployments/${s}`,{credentials:"include"}).then(c=>c.ok?c.json():null).then(c=>{if(c&&c.status==="succeeded")return r(!0,c);if(c&&c.status==="failed")return r(!1,c);r(!1,{error_message:"stream closed before terminal state"})}).catch(()=>r(!1,{error_message:"stream closed; deployment status unknown"}))}}),ht=()=>{const s={};for(const e of D.value){const a=(e.name||"").trim();a&&(s[a]=e.value??"")}return s},Xt=(s,e,a)=>{const t=(e||"POST").toUpperCase();return t==="GET"||t==="HEAD"||!a||Object.keys(s).some(c=>c.toLowerCase()==="content-type")||(s["Content-Type"]="application/json"),s},Qt=async()=>{if(!m.value){y.value="No function deployed yet";return}B.value="test",q.value=!0,ye.value=!0,A.value=null,y.value=null,ne.value=[],me.value=!1;try{const s=Xt(ht(),z.value,M.value);s["X-Orva-API-Key"]=s["X-Orva-API-Key"]||_o();const e=await wo(m.value,{method:z.value,path:J.value||"/",headers:s,body:M.value||""}),a=typeof e.data=="string"?e.data:JSON.stringify(e.data);X.value=`${e.status}`,xe.value=e.headers?.["x-orva-duration-ms"]||e.headers?.["X-Orva-Duration-MS"]||"?";try{A.value=JSON.stringify(JSON.parse(a),null,2)}catch{A.value=a}typeof e.status=="number"&&e.status>=500&&(me.value=!0)}catch(s){if(s.response){X.value=`${s.response.status}`;const e=s.response.data;y.value=typeof e=="string"?e:JSON.stringify(e),s.response.status>=500&&(me.value=!0)}else y.value=s.message||"Invocation failed",X.value="Error",me.value=!0}finally{ye.value=!1}},Zt=async()=>{if(!we.value){we.value=!0;try{const s=(ne.value||[]).join(` +`),e={};for(const c of D.value||[])c?.name&&c.name.trim()&&(e[c.name.trim()]=c.value||"");const a={method:z.value||"POST",path:J.value||"/",headers:e,body:M.value||""},t=/^\d+$/.test(X.value)?Number(X.value):X.value;await Lo({source:k.value||"",runtime:n.value.runtime||"",stderr:s,requestPreview:a,errorMessage:y.value||"",statusCode:t||""})?w.notify({title:"Prompt copied",message:"Paste into ChatGPT, Claude, or your AI tool of choice."}):w.notify({title:"Copy failed",message:"Could not write to the clipboard. Try again, or copy the stderr by hand.",danger:!0})}finally{we.value=!1}}},eo=()=>{D.value.push({name:"",value:""}),K.value=!0},to=s=>{D.value.splice(s,1)},Ce=async()=>{if(m.value)try{const s=await Co(m.value);W.value=s.data?.fixtures||[]}catch{W.value=[]}},oo=async()=>{se.value=!se.value,se.value&&await Ce()},so=s=>{z.value=s.method||"POST",J.value=s.path||"/",D.value=Object.entries(s.headers||{}).map(([e,a])=>({name:e,value:a})),D.value.length&&(K.value=!0),M.value=s.body||"",se.value=!1},no=async s=>{if(await w.ask({title:`Delete fixture "${s.name}"?`,message:"This only removes the saved request preset. Function code and execution history are untouched.",confirmLabel:"Delete",danger:!0}))try{await Do(m.value,s.name),await Ce()}catch(a){w.notify({title:"Delete failed",message:a.response?.data?.error?.message||a.message,danger:!0})}},ro=async()=>{if(!m.value)return;const e=(await w.prompt({title:"Save fixture",message:"Give this request a name so you can replay or share it later.",placeholder:"e.g. happy-path, signed-stripe, empty-body",confirmLabel:"Save"})||"").trim();if(!e)return;const a=ht(),t={name:e,method:z.value,path:J.value||"/",headers:a,body:M.value||""};try{await ko(m.value,e,t),se.value=!1,await Ce()}catch(r){w.notify({title:"Save failed",message:r.response?.data?.error?.message||r.message,danger:!0})}};he(m,async s=>{s?await Ce():W.value=[]});const Te=v(!1),ao=async s=>{if(!m.value||!s?.deployment_id||Te.value)return;let e=`Code hash ${s.short_hash}. Your current version stays in the history.`;try{const[t,r]=await Promise.all([T.get(`/deployments/${s.deployment_id}`),T.get("/functions")]),c=t.data?.snapshot,N=(r.data.functions||[]).find(G=>G.id===m.value);if(c&&N){const G=$o(N,c);G.length?e=`Rolling back to v${s.version} (code ${s.short_hash}) will also change: ${G.join(` `)} Secrets keep their current values; they aren't part of the rollback.`:e=`Rolling back to v${s.version} (code ${s.short_hash}). Settings and env are already identical, so only the code changes.`}}catch{}if(ve.value&&ve.value!==s.deployment_id&&U.params.name&&(e+=` -Full source diff: ${window.location.origin}/web/functions/${U.params.name}/diff?from=${s.deployment_id}&to=${ve.value}`),!!await w.ask({title:`Restore v${s.version}?`,message:e,confirmLabel:"Rollback"})){Te.value=!0;try{await Io(m.value,{deployment_id:s.deployment_id});const r=((await T.get("/functions")).data.functions||[]).find(c=>c.id===m.value);r&&(n.value.runtime=r.runtime,n.value.memory_mb=r.memory_mb,n.value.cpus=r.cpus,n.value.network_mode=r.network_mode||"none",n.value.max_concurrency=r.max_concurrency||0,n.value.concurrency_policy=r.concurrency_policy||"queue",n.value.auth_mode=r.auth_mode||"none",n.value.rate_limit_per_min=r.rate_limit_per_min||0,n.value.description=r.description||"",r.env_vars&&Object.keys(r.env_vars).length>0?F.value=Object.entries(r.env_vars).map(([c,N])=>({key:c,value:N})):F.value=[],await Ve(r),await Le(r))}catch(t){const r=t.response?.data?.error?.code||"",c=t.response?.data?.error?.message||t.message||"Rollback failed";r==="VERSION_GCD"?w.notify({title:"Version unavailable",message:`This version has been garbage-collected and can no longer be restored. +Full source diff: ${window.location.origin}/web/functions/${U.params.name}/diff?from=${s.deployment_id}&to=${ve.value}`),!!await w.ask({title:`Restore v${s.version}?`,message:e,confirmLabel:"Rollback"})){Te.value=!0;try{await Io(m.value,{deployment_id:s.deployment_id});const r=((await T.get("/functions")).data.functions||[]).find(c=>c.id===m.value);r&&(n.value.runtime=r.runtime,n.value.memory_mb=r.memory_mb,n.value.cpus=r.cpus,n.value.network_mode=r.network_mode||"none",n.value.max_concurrency=r.max_concurrency||0,n.value.concurrency_policy=r.concurrency_policy||"queue",n.value.auth_mode=r.auth_mode||"none",n.value.rate_limit_per_min=r.rate_limit_per_min||0,n.value.description=r.description||"",r.env_vars&&Object.keys(r.env_vars).length>0?F.value=Object.entries(r.env_vars).map(([c,N])=>({key:c,value:N})):F.value=[],await Me(r),await Le(r))}catch(t){const r=t.response?.data?.error?.code||"",c=t.response?.data?.error?.message||t.message||"Rollback failed";r==="VERSION_GCD"?w.notify({title:"Version unavailable",message:`This version has been garbage-collected and can no longer be restored. -${c}`,danger:!0}):w.notify({title:"Rollback failed",message:c,danger:!0})}finally{Te.value=!1}}},Ee=async()=>{if(m.value)try{const s=await T.get(`/functions/${m.value}/secrets`);je.value=(s.data.secrets||[]).map(e=>({id:e,name:e}))}catch(s){console.error("Failed to load secrets",s)}},io=async()=>{const s=P.value.name.trim();if(s){if(!m.value){H.value=H.value.filter(e=>e.name!==s),H.value.push({name:s,value:P.value.value}),P.value.name="",P.value.value="";return}try{await T.post(`/functions/${m.value}/secrets`,{key:s,value:P.value.value}),P.value.name="",P.value.value="",await Ee()}catch(e){y.value=e.response?.data?.error?.message||"Failed to save secret"}}},lo=s=>{H.value.splice(s,1)},uo=async s=>{if(!(!m.value||!await w.ask({title:"Delete secret?",message:`"${s}" will be removed from this function's environment.`,confirmLabel:"Delete",danger:!0})))try{await T.delete(`/functions/${m.value}/secrets/${encodeURIComponent(s)}`),await Ee()}catch(a){y.value=a.response?.data?.error?.message||"Failed to delete secret"}},co=async()=>{if(!(!m.value||!H.value.length)){for(const s of H.value)try{await T.post(`/functions/${m.value}/secrets`,{key:s.name,value:s.value}),x.value.push(`Saved secret: ${s.name}`)}catch(e){const a=e.response?.data?.error?.message||e.message;x.value.push(`Failed to save secret ${s.name}: ${a}`)}H.value=[],await Ee()}},mo=async()=>{await w.ask({title:"Reset editor?",message:"Unsaved changes in the editor will be discarded.",confirmLabel:"Reset",danger:!0})&&(n.value.name="",m.value="",k.value="",ce.value=!1,A.value=null,y.value=null,Pe("python314"))};return(s,e)=>{const a=yo("router-link");return i(),l("div",ws,[o("h1",ks,p(n.value.name||"New function"),1),o("div",Ss,[o("div",Cs,[d(f(bt),{class:"w-4 h-4 text-foreground-muted shrink-0"}),_(o("input",{"onUpdate:modelValue":e[0]||(e[0]=t=>n.value.name=t),placeholder:"my-function",disabled:R.value,class:"bg-transparent border-0 text-base sm:text-sm font-medium text-white placeholder-foreground-muted focus:outline-none px-1 py-1 min-w-0 flex-1 sm:flex-none sm:w-40"},null,8,Ts),[[O,n.value.name]]),!R.value&&!m.value?(i(),l("button",{key:0,type:"button",class:"p-1 rounded text-foreground-muted hover:text-white hover:bg-surface-hover transition-colors shrink-0 touch-expand-iconbtn",title:"Re-roll a fresh name","aria-label":"Re-roll a fresh name",onClick:mt},[d(f(Tt),{class:"w-3.5 h-3.5"})])):h("",!0),o("span",Es,p(Pt(n.value.runtime)),1)]),o("div",Os,[o("div",{class:"relative",ref_key:"configMenuRef",ref:Ye},[o("button",{type:"button",class:"panel-btn","aria-haspopup":"menu","aria-expanded":j.value.config,onClick:e[1]||(e[1]=t=>Qe("config"))},[d(f(Ue),{class:"w-3.5 h-3.5"}),e[51]||(e[51]=u(" Config ",-1)),d(f(be),{class:"w-3 h-3 text-foreground-muted"})],8,Rs),j.value.config?(i(),l("div",js,[o("button",{class:"menu-item",role:"menuitem",onClick:e[2]||(e[2]=t=>ae("settings"))},[d(f(Ue),{class:"w-3.5 h-3.5"}),e[52]||(e[52]=o("span",{class:"flex-1 text-left"},"Settings",-1)),e[53]||(e[53]=o("span",{class:"text-[10px] text-foreground-muted"},"runtime · limits",-1))]),o("button",{class:"menu-item",role:"menuitem",onClick:e[3]||(e[3]=t=>ae("envVars"))},[d(f(xt),{class:"w-3.5 h-3.5"}),e[54]||(e[54]=o("span",{class:"flex-1 text-left"},"Env",-1)),ot.value?(i(),l("span",Ns,p(ot.value),1)):h("",!0)]),o("button",{class:"menu-item",role:"menuitem",onClick:e[4]||(e[4]=t=>ae("deps"))},[d(f(yt),{class:"w-3.5 h-3.5"}),e[55]||(e[55]=o("span",{class:"flex-1 text-left"},"Deps",-1)),e[56]||(e[56]=o("span",{class:"text-[10px] text-foreground-muted"},"package · requirements",-1))]),o("button",{class:"menu-item",role:"menuitem",onClick:e[5]||(e[5]=t=>ae("secrets"))},[d(f(_t),{class:"w-3.5 h-3.5"}),e[57]||(e[57]=o("span",{class:"flex-1 text-left"},"Secrets",-1)),Ae.value?(i(),l("span",Ds,p(Ae.value),1)):h("",!0)]),R.value&&ie.value.length?(i(),l("button",{key:0,class:"menu-item",role:"menuitem",onClick:e[6]||(e[6]=t=>ae("versions"))},[d(f(Ct),{class:"w-3.5 h-3.5"}),e[58]||(e[58]=o("span",{class:"flex-1 text-left"},"Versions",-1)),o("span",As,p(ie.value.length),1)])):h("",!0)])):h("",!0)],512),o("div",{class:"relative",ref_key:"bindingsMenuRef",ref:Xe},[o("button",{type:"button",class:"panel-btn","aria-haspopup":"menu","aria-expanded":j.value.bindings,onClick:e[7]||(e[7]=t=>Qe("bindings"))},[d(f(ho),{class:"w-3.5 h-3.5"}),e[59]||(e[59]=u(" Bindings ",-1)),d(f(be),{class:"w-3 h-3 text-foreground-muted"})],8,Ps),j.value.bindings?(i(),l("div",Is,[R.value?(i(),l("button",{key:0,class:"menu-item",role:"menuitem",onClick:e[8]||(e[8]=t=>Ze({name:"function-kv",params:{name:n.value.name}}))},[d(f(Ko),{class:"w-3.5 h-3.5"}),e[60]||(e[60]=o("span",{class:"flex-1 text-left"},"KV store",-1)),e[61]||(e[61]=o("span",{class:"text-[10px] text-foreground-muted"},"per-function state",-1))])):h("",!0),R.value?(i(),l("button",{key:1,class:"menu-item",role:"menuitem",onClick:e[9]||(e[9]=t=>Ze({name:"function-inbound-webhooks",params:{name:n.value.name}}))},[d(f(go),{class:"w-3.5 h-3.5"}),e[62]||(e[62]=o("span",{class:"flex-1 text-left"},"Inbound webhooks",-1)),e[63]||(e[63]=o("span",{class:"text-[10px] text-foreground-muted"},"signed POST",-1))])):h("",!0),o("button",{class:"menu-item",role:"menuitem",onClick:e[10]||(e[10]=t=>ae("docs"))},[d(f(wt),{class:"w-3.5 h-3.5"}),e[64]||(e[64]=o("span",{class:"flex-1 text-left"},"Docs",-1)),e[65]||(e[65]=o("span",{class:"text-[10px] text-foreground-muted"},"handler reference",-1))])])):h("",!0)],512),e[67]||(e[67]=o("div",{class:"w-px h-5 bg-border mx-1"},null,-1)),d(M,{variant:"secondary",size:"sm",onClick:mo},{default:g(()=>[...e[66]||(e[66]=[u(" Reset ",-1)])]),_:1}),d(M,{size:"sm",loading:Y.value,onClick:pt},{default:g(()=>[d(f(qe),{class:"w-4 h-4"}),u(" "+p(R.value?"Deploy New Version":"Deploy"),1)]),_:1},8,["loading"])])]),m.value?(i(),l("div",Ms,[e[69]||(e[69]=o("span",{class:"text-foreground-muted shrink-0 uppercase tracking-wider text-[10px]"},"Invoke URL",-1)),o("code",Vs,p(ke.value),1),o("button",{class:"px-2 py-1 rounded text-foreground-muted hover:text-white hover:bg-surface-hover transition-colors flex items-center gap-1 shrink-0",onClick:Mt},[_e.value?(i(),le(f(qo),{key:0,class:"w-3 h-3 text-success"})):(i(),le(f(Bo),{key:1,class:"w-3 h-3"})),u(" "+p(_e.value?"Copied":"Copy"),1)]),R.value&&n.value.name?(i(),le(a,{key:0,to:`/functions/${n.value.name}/deployments`,class:"text-foreground-muted hover:text-white transition-colors px-2"},{default:g(()=>[...e[68]||(e[68]=[u(" Deployments → ",-1)])]),_:1},8,["to"])):h("",!0)])):h("",!0),o("div",Ls,[o("div",$s,[o("div",Us,[d(f(bt),{class:"w-3 h-3"}),o("span",qs,p(it.value),1),Z.value?(i(),l("span",Bs,"· template: "+p(Z.value),1)):h("",!0)]),o("div",zs,p(k.value.length)+" chars ",1)]),d(f(Dt),{modelValue:k.value,"onUpdate:modelValue":e[11]||(e[11]=t=>k.value=t),language:n.value.runtime,class:"flex-1 min-h-0"},null,8,["modelValue","language"])]),o("div",Fs,[o("div",Hs,[(i(!0),l(S,null,C(At.value,t=>(i(),l("button",{key:t.id,class:oe(["px-3 h-9 text-xs flex items-center gap-1.5 border-b-2 transition-colors",B.value===t.id?"text-white border-primary":"text-foreground-muted border-transparent hover:text-white"]),onClick:r=>{B.value=t.id,q.value=!0}},[(i(),le(Oo(t.icon),{class:"w-3 h-3"})),u(" "+p(t.label)+" ",1),t.badge?(i(),l("span",Js,p(t.badge),1)):h("",!0)],10,Gs))),128)),o("div",Ks,[B.value==="test"?(i(),l("button",{key:0,disabled:!$.value||ye.value,class:"run-btn",title:$.value?"Invoke with the payload below":"Deploy first",onClick:Qt},[ye.value?(i(),l("span",Ys)):(i(),le(f(kt),{key:0,class:"w-3 h-3"})),e[70]||(e[70]=u(" Run ",-1))],8,Ws)):h("",!0),o("button",{class:"p-1.5 rounded text-foreground-muted hover:text-white hover:bg-surface-hover transition-colors",title:q.value?"Collapse":"Expand",onClick:e[12]||(e[12]=t=>q.value=!q.value)},[d(f(be),{class:oe(["w-4 h-4 transition-transform",q.value?"rotate-0":"rotate-180"])},null,8,["class"])],8,Xs)])]),_(o("div",Qs,[B.value==="build"?(i(),l("div",Zs,[x.value.length?h("",!0):(i(),l("div",en," No build activity yet. Deploy the function to stream logs here. ")),(i(!0),l(S,null,C(x.value,(t,r)=>(i(),l("div",{key:r,class:"text-foreground-muted whitespace-pre-wrap break-words"},p(t),1))),128))])):B.value==="test"?(i(),l("div",tn,[o("div",on,[o("div",sn,[_(o("select",{"onUpdate:modelValue":e[13]||(e[13]=t=>z.value=t),disabled:!$.value,class:"text-[11px] font-mono bg-background border border-border rounded px-1.5 py-0.5 text-foreground focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50"},[(i(),l(S,null,C(It,t=>o("option",{key:t,value:t},p(t),9,rn)),64))],8,nn),[[Oe,z.value]]),_(o("input",{"onUpdate:modelValue":e[14]||(e[14]=t=>J.value=t),disabled:!$.value,spellcheck:"false",placeholder:"/",class:"flex-1 min-w-0 text-[11px] font-mono bg-background border border-border rounded px-2 py-0.5 text-foreground focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50"},null,8,an),[[O,J.value]]),o("div",ln,[o("button",{type:"button",class:"text-[11px] font-medium text-foreground-muted hover:text-white px-1.5 py-0.5 rounded hover:bg-surface-hover transition-colors flex items-center gap-1",disabled:!m.value,title:m.value?"Saved fixtures for this function":"Deploy first",onClick:oo},[e[71]||(e[71]=u(" Saved ",-1)),W.value.length?(i(),l("span",un,"· "+p(W.value.length),1)):h("",!0),d(f(be),{class:"w-3 h-3"})],8,dn),se.value?(i(),l("div",{key:0,class:"absolute right-0 top-full mt-1 z-30 w-64 bg-surface border border-border rounded shadow-lg overflow-hidden",onMouseleave:e[15]||(e[15]=t=>se.value=!1)},[e[72]||(e[72]=o("div",{class:"px-3 py-2 text-[10px] uppercase tracking-[0.14em] text-foreground-muted/80 border-b border-border bg-surface/60"}," Saved fixtures ",-1)),W.value.length?(i(),l("ul",mn,[(i(!0),l(S,null,C(W.value,t=>(i(),l("li",{key:t.id,class:"flex items-center gap-2 px-3 py-1.5 text-xs hover:bg-surface-hover cursor-pointer group",onClick:r=>so(t)},[o("span",fn,p(t.method),1),o("span",vn,p(t.name),1),o("button",{type:"button",class:"opacity-100 lg:opacity-0 lg:group-hover:opacity-100 text-foreground-muted hover:text-red-400 transition-opacity",title:`Delete ${t.name}`,"aria-label":`Delete ${t.name}`,onClick:Ro(r=>no(t),["stop"])},[d(f(Be),{class:"w-3 h-3"})],8,hn)],8,pn))),128))])):(i(),l("div",cn," No fixtures yet. Set up a request and click Save. ")),o("div",gn,[o("button",{type:"button",class:"text-[11px] text-foreground hover:text-white w-full text-left px-1.5 py-1 rounded hover:bg-surface-hover transition-colors disabled:opacity-50",disabled:!$.value,onClick:ro}," + Save current as… ",8,bn)])],32)):h("",!0)])]),o("div",yn,[o("button",{type:"button",class:"w-full h-6 px-3 flex items-center justify-between text-[10px] uppercase tracking-[0.14em] text-foreground-muted hover:text-white bg-surface/30 transition-colors",onClick:e[16]||(e[16]=t=>K.value=!K.value)},[o("span",null,[e[73]||(e[73]=u(" Headers ",-1)),st.value?(i(),l("span",xn,"· "+p(st.value),1)):h("",!0)]),d(f(be),{class:oe(["w-3 h-3 transition-transform",K.value?"rotate-0":"-rotate-90"])},null,8,["class"])]),K.value?(i(),l("div",_n,[(i(!0),l(S,null,C(D.value,(t,r)=>(i(),l("div",{key:r,class:"flex items-center gap-1.5"},[_(o("input",{"onUpdate:modelValue":c=>t.name=c,disabled:!$.value,spellcheck:"false",placeholder:"Header name",class:"flex-1 min-w-0 text-[11px] font-mono bg-background border border-border rounded px-2 py-0.5 text-foreground focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50"},null,8,wn),[[O,t.name]]),_(o("input",{"onUpdate:modelValue":c=>t.value=c,disabled:!$.value,spellcheck:"false",placeholder:"value",class:"flex-1 min-w-0 text-[11px] font-mono bg-background border border-border rounded px-2 py-0.5 text-foreground focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50"},null,8,kn),[[O,t.value]]),o("button",{type:"button",class:"text-foreground-muted hover:text-red-400 p-0.5 transition-colors",title:"Remove header",onClick:c=>to(r)},[d(f($e),{class:"w-3 h-3"})],8,Sn)]))),128)),o("button",{type:"button",class:"text-[11px] text-foreground-muted hover:text-white transition-colors px-1.5 py-0.5 rounded hover:bg-surface-hover",disabled:!$.value,onClick:eo}," + Add header ",8,Cn)])):h("",!0)]),o("div",Tn,[e[74]||(e[74]=o("span",{class:"text-[10px] uppercase tracking-[0.14em] font-medium text-foreground-muted"}," Body ",-1)),$.value?(i(),l("span",On,p(V.value.length)+" chars",1)):(i(),l("span",En,"Deploy first"))]),_(o("textarea",{"onUpdate:modelValue":e[17]||(e[17]=t=>V.value=t),disabled:!$.value,spellcheck:"false",class:"flex-1 w-full min-h-0 bg-background text-xs font-mono p-3 text-foreground focus:outline-none resize-none disabled:opacity-50 placeholder:text-foreground-muted/50",placeholder:"{}"},null,8,Rn),[[O,V.value]])]),o("div",jn,[o("div",Nn,[o("span",{class:oe(["text-[10px] uppercase tracking-[0.14em] font-medium flex items-center gap-1.5",y.value?"text-red-400":A.value?"text-success":"text-foreground-muted"])},[o("span",{class:oe(["w-1.5 h-1.5 rounded-full",y.value?"bg-red-400":A.value?"bg-success":"bg-foreground-muted/40"])},null,2),u(" "+p(y.value?"Error":A.value?"Response":"Idle"),1)],2),o("div",Dn,[me.value?(i(),l("button",{key:0,type:"button",class:"text-[10px] uppercase tracking-[0.14em] text-foreground-muted hover:text-white px-1.5 py-0.5 rounded hover:bg-surface-hover transition-colors flex items-center gap-1 disabled:opacity-50",disabled:we.value,title:"Build a paste-ready debug prompt with source + request + stderr",onClick:Zt},[d(f(zo),{class:"w-3 h-3"}),e[75]||(e[75]=u(" Suggest fix ",-1))],8,An)):h("",!0),xe.value?(i(),l("span",Pn,p(X.value)+" · "+p(xe.value)+"ms",1)):h("",!0)])]),o("div",In,[A.value||y.value?(i(),l("pre",{key:0,class:oe(["px-3 py-2.5 font-mono text-xs whitespace-pre-wrap break-all leading-relaxed",y.value?"text-red-200":"text-foreground"])},p(A.value||y.value),3)):(i(),l("div",Mn,[...e[76]||(e[76]=[u(" Hit ",-1),o("span",{class:"not-italic text-white"},"Run",-1),u(" to invoke this function with the request payload. ",-1)])])),ne.value.length?(i(),l("div",Vn,[o("div",Ln," Function logs · "+p(ne.value.length),1),o("div",$n,[(i(!0),l(S,null,C(ne.value,(t,r)=>(i(),l("div",{key:r,class:"text-foreground-muted whitespace-pre-wrap break-words"},p(t),1))),128))])])):h("",!0)])])])):h("",!0)],512),[[bo,q.value]])]),d(re,{modelValue:b.value.settings,"onUpdate:modelValue":e[30]||(e[30]=t=>b.value.settings=t),title:"Function configuration",icon:f(Ue),size:"md"},{footer:g(()=>[d(M,{variant:"secondary",onClick:e[29]||(e[29]=t=>b.value.settings=!1)},{default:g(()=>[...e[103]||(e[103]=[u(" Done ",-1)])]),_:1})]),default:g(()=>[o("div",Un,[o("div",null,[e[77]||(e[77]=o("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"Description",-1)),_(o("textarea",{"onUpdate:modelValue":e[18]||(e[18]=t=>n.value.description=t),rows:"2",placeholder:"One-line summary of what this function does. Surfaces in MCP tool catalogs and the agent channel picker.",class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:border-white resize-y"},null,512),[[O,n.value.description]])]),o("div",null,[o("label",qn,[e[78]||(e[78]=o("span",null,"Runtime",-1)),fe.value&&!pe.value?(i(),l("span",Bn,"auto-detected")):h("",!0)]),o("div",zn,[(i(),l(S,null,C(rt,t=>o("button",{key:t.id,class:oe(["px-2 py-2 rounded border text-xs font-medium transition-colors duration-150 flex items-center justify-center",n.value.runtime===t.id?"bg-white text-black border-white":"bg-surface-hover text-foreground-muted border-border hover:border-foreground-muted"]),onClick:r=>lt(t.id)},p(t.label),11,Fn)),64))])]),o("div",null,[e[80]||(e[80]=o("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"Template",-1)),_(o("select",{"onUpdate:modelValue":e[19]||(e[19]=t=>Z.value=t),class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onChange:Bt},[e[79]||(e[79]=o("option",{value:""}," Custom (blank) ",-1)),(i(!0),l(S,null,C(zt.value,t=>(i(),l("optgroup",{key:t.label,label:t.label},[(i(!0),l(S,null,C(t.items,r=>(i(),l("option",{key:r.id,value:r.id},p(r.label)+p(r.cron?" · scheduled":"")+": "+p(r.description),9,Gn))),128))],8,Hn))),128))],544),[[Oe,Z.value]]),dt.value?(i(),l("p",Jn,p(dt.value),1)):h("",!0)]),o("div",Kn,[d(de,{modelValue:n.value.memory_mb,"onUpdate:modelValue":e[20]||(e[20]=t=>n.value.memory_mb=t),modelModifiers:{number:!0},label:"Memory (MB)",type:"number",placeholder:"64"},null,8,["modelValue"]),d(de,{modelValue:n.value.cpus,"onUpdate:modelValue":e[21]||(e[21]=t=>n.value.cpus=t),modelModifiers:{number:!0},label:"CPUs",type:"number",placeholder:"0.5"},null,8,["modelValue"])]),o("div",Wn,[e[83]||(e[83]=o("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block"},"Concurrency",-1)),o("div",Yn,[o("div",null,[d(de,{modelValue:n.value.max_concurrency,"onUpdate:modelValue":e[22]||(e[22]=t=>n.value.max_concurrency=t),modelModifiers:{number:!0},label:"Max concurrent (0 = unlimited)",type:"number",placeholder:"0"},null,8,["modelValue"])]),o("div",null,[e[82]||(e[82]=o("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"When at cap",-1)),_(o("select",{"onUpdate:modelValue":e[23]||(e[23]=t=>n.value.concurrency_policy=t),disabled:!n.value.max_concurrency,class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white disabled:opacity-50"},[...e[81]||(e[81]=[o("option",{value:"queue"}," Queue requests ",-1),o("option",{value:"reject"}," Reject (429) ",-1)])],8,Xn),[[Oe,n.value.concurrency_policy]])])]),e[84]||(e[84]=o("p",{class:"text-[11px] text-foreground-muted leading-snug"}," Caps how many in-flight invocations one function can have. Use this to protect downstream APIs from a runaway handler. ",-1))]),o("div",Qn,[o("label",Zn,[_(o("input",{"onUpdate:modelValue":e[24]||(e[24]=t=>n.value.network_mode=t),type:"checkbox","true-value":"egress","false-value":"none",class:"mt-0.5 w-4 h-4 rounded border-border bg-background"},null,512),[[jo,n.value.network_mode]]),o("div",er,[o("div",tr,[d(f(St),{class:"w-4 h-4 text-foreground-muted"}),e[85]||(e[85]=u(" Allow outbound network ",-1))]),e[86]||(e[86]=o("div",{class:"text-xs text-foreground-muted mt-1 leading-snug"}," Off by default. Turn on if this function needs to call external APIs (Stripe, OpenAI, your DB). Adds ~5 ms cold-start. ",-1))])])]),o("div",or,[o("label",sr,[d(f(Ho),{class:"w-3.5 h-3.5"}),e[87]||(e[87]=u(" Invoke gate ",-1))]),_(o("select",{"onUpdate:modelValue":e[25]||(e[25]=t=>n.value.auth_mode=t),class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white"},[...e[88]||(e[88]=[o("option",{value:"none"}," Public, anyone can invoke ",-1),o("option",{value:"platform_key"}," Require Orva API key (server-to-server) ",-1),o("option",{value:"signed"}," Require HMAC signature (X-Orva-Signature) ",-1)])],512),[[Oe,n.value.auth_mode]]),e[89]||(e[89]=o("p",{class:"text-[11px] text-foreground-muted leading-snug"},[u(" Public is the default, matches Cloudflare Workers and Vercel Functions. For end-user auth (JWT, session cookies), keep this on "),o("span",{class:"text-white"},"Public"),u(" and verify inside your handler. "),o("span",{class:"text-white"},"Signed"),u(" mode reads its key from the function secret "),o("span",{class:"font-mono"},"ORVA_SIGNING_SECRET"),u(". ")],-1))]),o("div",nr,[e[90]||(e[90]=o("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block"},"Rate limit",-1)),d(de,{modelValue:n.value.rate_limit_per_min,"onUpdate:modelValue":e[26]||(e[26]=t=>n.value.rate_limit_per_min=t),modelModifiers:{number:!0},label:"Requests per minute, per IP (0 = unlimited)",type:"number",placeholder:"0"},null,8,["modelValue"]),e[91]||(e[91]=o("p",{class:"text-[11px] text-foreground-muted leading-snug"},[u(" Token-bucket per client IP. A burst up to the limit is allowed, then refills at rate/60 per second. Returns 429 with "),o("span",{class:"font-mono"},"Retry-After: 60"),u(" when exceeded. ")],-1))]),o("div",rr,[o("label",ar,[d(f(St),{class:"w-3.5 h-3.5"}),e[92]||(e[92]=u(" Custom routes ",-1)),De.value?(i(),l("span",ir,"loading…")):h("",!0)]),m.value?nt.value.length===0?(i(),l("div",dr,[e[93]||(e[93]=u(" No custom routes for this function. Default invoke URL is ",-1)),o("span",ur,"/fn/"+p(m.value.slice(0,8))+"…",1),e[94]||(e[94]=u(". Add a pretty path below (e.g. ",-1)),e[95]||(e[95]=o("span",{class:"font-mono"},"/webhooks/stripe",-1)),e[96]||(e[96]=u(" or ",-1)),e[97]||(e[97]=o("span",{class:"font-mono"},"/api/payments/*",-1)),e[98]||(e[98]=u(" for prefix match). ",-1))])):(i(),l("ul",cr,[(i(!0),l(S,null,C(nt.value,t=>(i(),l("li",{key:t.path,class:"flex items-center gap-2 px-2.5 py-1.5 rounded border border-border bg-surface-hover/50"},[o("code",mr,p(t.path),1),t.methods&&t.methods!=="*"?(i(),l("span",pr,p(t.methods),1)):h("",!0),o("button",{class:"shrink-0 w-6 h-6 flex items-center justify-center rounded text-foreground-muted hover:text-red-400 hover:bg-surface transition-colors",title:"Remove route",onClick:r=>Jt(t.path)},[d(f($e),{class:"w-3 h-3"})],8,fr)]))),128))])):(i(),l("p",lr," Save the function first. Custom routes need a target function id. ")),m.value?(i(),l("div",vr,[o("div",hr,[_(o("input",{"onUpdate:modelValue":e[27]||(e[27]=t=>ee.value.path=t),placeholder:"/path or /prefix/*",class:"flex-1 min-w-0 bg-background border border-border rounded-md px-2 py-1.5 text-xs font-mono text-foreground focus:outline-none focus:border-white"},null,512),[[O,ee.value.path]]),_(o("input",{"onUpdate:modelValue":e[28]||(e[28]=t=>ee.value.methods=t),placeholder:"*",title:"Comma-separated methods or * for any (default *)",class:"w-20 bg-background border border-border rounded-md px-2 py-1.5 text-xs font-mono text-foreground focus:outline-none focus:border-white"},null,512),[[O,ee.value.methods]]),o("button",{class:"shrink-0 px-3 py-1.5 rounded-md bg-white text-black text-xs font-medium hover:bg-white/90 transition-colors",onClick:Gt}," Add ")]),L.value?(i(),l("p",gr,[e[99]||(e[99]=u(" ⚠ ",-1)),o("span",br,p(L.value.path),1),e[100]||(e[100]=u(" already maps to function ",-1)),o("span",yr,p(L.value.currentFunctionId.slice(0,8))+"…",1),e[101]||(e[101]=u("; clicking Add will remap it to this one. ",-1))])):h("",!0),I.value?(i(),l("p",xr,p(I.value),1)):h("",!0),e[102]||(e[102]=o("p",{class:"text-[11px] text-foreground-muted leading-snug"},[u(" Reserved prefixes: "),o("span",{class:"font-mono"},"/api/"),u(", "),o("span",{class:"font-mono"},"/auth/"),u(", "),o("span",{class:"font-mono"},"/web/"),u(", "),o("span",{class:"font-mono"},"/_orva/"),u(". Prefix routes must end in "),o("span",{class:"font-mono"},"/*"),u(". ")],-1))])):h("",!0)])])]),_:1},8,["modelValue","icon"]),d(re,{modelValue:b.value.envVars,"onUpdate:modelValue":e[32]||(e[32]=t=>b.value.envVars=t),title:"Environment variables",icon:f(xt),size:"md"},{footer:g(()=>[d(M,{variant:"secondary",onClick:e[31]||(e[31]=t=>b.value.envVars=!1)},{default:g(()=>[...e[105]||(e[105]=[u(" Done ",-1)])]),_:1})]),default:g(()=>[o("div",_r,[(i(!0),l(S,null,C(F.value,(t,r)=>(i(),l("div",{key:r,class:"flex items-center gap-2"},[_(o("input",{"onUpdate:modelValue":c=>t.key=c,placeholder:"KEY",class:"flex-1 min-w-0 bg-background border border-border rounded-md px-2 py-1.5 text-xs text-foreground focus:outline-none focus:border-white"},null,8,wr),[[O,t.key]]),_(o("input",{"onUpdate:modelValue":c=>t.value=c,placeholder:"VALUE",class:"flex-1 min-w-0 bg-background border border-border rounded-md px-2 py-1.5 text-xs text-foreground focus:outline-none focus:border-white"},null,8,kr),[[O,t.value]]),o("button",{class:"shrink-0 w-7 h-7 flex items-center justify-center rounded text-foreground-muted hover:text-red-400 hover:bg-surface transition-colors",title:"Remove",onClick:c=>Ht(r)},[d(f($e),{class:"w-3.5 h-3.5"})],8,Sr)]))),128)),o("button",{class:"text-xs text-foreground-muted hover:text-white transition-colors",onClick:Ft}," + Add variable "),e[104]||(e[104]=o("p",{class:"text-[11px] text-foreground-muted pt-2 border-t border-border"},[u(" Plaintext at deploy time. Use "),o("span",{class:"text-white"},"Secrets"),u(" for sensitive values. ")],-1))])]),_:1},8,["modelValue","icon"]),d(re,{modelValue:b.value.deps,"onUpdate:modelValue":e[35]||(e[35]=t=>b.value.deps=t),title:"Dependencies",icon:f(yt),size:"md"},{footer:g(()=>[d(M,{variant:"secondary",onClick:e[34]||(e[34]=t=>b.value.deps=!1)},{default:g(()=>[...e[106]||(e[106]=[u(" Done ",-1)])]),_:1})]),default:g(()=>[o("div",Cr,[o("div",Tr,p($t.value),1),_(o("textarea",{"onUpdate:modelValue":e[33]||(e[33]=t=>Q.value=t),class:"w-full bg-surface-hover border border-border rounded-md text-xs font-mono p-3 text-foreground focus:outline-none focus:border-white resize-none min-h-[200px]",placeholder:"One package per line. e.g. requests==2.31.0"},null,512),[[O,Q.value]])])]),_:1},8,["modelValue","icon"]),d(re,{modelValue:b.value.secrets,"onUpdate:modelValue":e[39]||(e[39]=t=>b.value.secrets=t),title:"Secrets",icon:f(_t),size:"md"},{footer:g(()=>[d(M,{variant:"secondary",onClick:e[38]||(e[38]=t=>b.value.secrets=!1)},{default:g(()=>[...e[109]||(e[109]=[u(" Done ",-1)])]),_:1})]),default:g(()=>[o("div",Er,[Ae.value?h("",!0):(i(),l("div",Or,[e[107]||(e[107]=u(" No secrets yet. Add a key-value pair below.",-1)),m.value?h("",!0):(i(),l("span",Rr," They'll be saved when you deploy."))])),(i(!0),l(S,null,C(je.value,t=>(i(),l("div",{key:t.id,class:"flex items-center justify-between text-xs px-3 py-2 rounded border border-border"},[o("span",jr,p(t.name),1),o("button",{class:"text-foreground-muted hover:text-red-400 transition-colors",onClick:r=>uo(t.id)},[d(f(Be),{class:"w-3.5 h-3.5"})],8,Nr)]))),128)),(i(!0),l(S,null,C(H.value,(t,r)=>(i(),l("div",{key:"pending-"+r,class:"flex items-center justify-between text-xs px-3 py-2 rounded border border-amber-500/30 bg-amber-500/5"},[o("div",Dr,[o("span",Ar,p(t.name),1),e[108]||(e[108]=o("span",{class:"text-[10px] uppercase tracking-wider text-amber-400/80"},"pending",-1))]),o("button",{class:"text-foreground-muted hover:text-red-400 transition-colors",onClick:c=>lo(r)},[d(f(Be),{class:"w-3.5 h-3.5"})],8,Pr)]))),128)),o("div",Ir,[_(o("input",{"onUpdate:modelValue":e[36]||(e[36]=t=>P.value.name=t),placeholder:"SECRET_NAME",class:"w-full bg-background border border-border rounded-md px-2 py-1.5 text-xs text-foreground focus:outline-none focus:border-white"},null,512),[[O,P.value.name]]),_(o("input",{"onUpdate:modelValue":e[37]||(e[37]=t=>P.value.value=t),placeholder:"SECRET_VALUE",type:"password",class:"w-full bg-background border border-border rounded-md px-2 py-1.5 text-xs text-foreground focus:outline-none focus:border-white"},null,512),[[O,P.value.value]]),d(M,{class:"w-full",variant:"secondary",onClick:io},{default:g(()=>[d(f(Vo),{class:"w-4 h-4"}),u(" "+p(m.value?"Save secret":"Queue secret for deploy"),1)]),_:1})])])]),_:1},8,["modelValue","icon"]),d(re,{modelValue:b.value.versions,"onUpdate:modelValue":e[42]||(e[42]=t=>b.value.versions=t),title:"Version history",icon:f(Ct),size:"md"},{footer:g(()=>[d(M,{variant:"secondary",onClick:e[41]||(e[41]=t=>b.value.versions=!1)},{default:g(()=>[...e[113]||(e[113]=[u(" Done ",-1)])]),_:1})]),default:g(()=>[o("div",Mr,[(i(!0),l(S,null,C(ie.value,t=>(i(),l("div",{key:t.deployment_id,class:"flex items-center justify-between gap-2 text-xs px-3 py-2 rounded border border-border"},[o("div",Vr,[o("span",Lr,"v"+p(t.version),1),t.is_active?(i(),l("span",$r,"Active")):h("",!0),o("span",{class:"font-mono text-foreground-muted truncate",title:t.code_hash},p(t.short_hash),9,Ur),e[110]||(e[110]=o("span",{class:"text-foreground-muted shrink-0"},"·",-1)),o("span",qr,p(new Date(t.created_at).toLocaleDateString()),1)]),t.is_active?h("",!0):(i(),l("div",Br,[ve.value?(i(),le(a,{key:0,to:{name:"function-diff",params:{name:f(U).params.name},query:{from:t.deployment_id,to:ve.value}},class:"text-foreground-muted hover:text-white flex items-center gap-1",title:"Compare with active version",onClick:e[40]||(e[40]=r=>b.value.versions=!1)},{default:g(()=>[d(f(Fo),{class:"w-3 h-3"}),e[111]||(e[111]=u(" Compare ",-1))]),_:1},8,["to"])):h("",!0),o("button",{disabled:Te.value,class:"text-foreground-muted hover:text-white disabled:opacity-50 flex items-center gap-1",onClick:r=>ao(t)},[d(f(Go),{class:"w-3 h-3"}),e[112]||(e[112]=u(" Rollback ",-1))],8,zr)]))]))),128))])]),_:1},8,["modelValue","icon"]),d(re,{modelValue:b.value.docs,"onUpdate:modelValue":e[45]||(e[45]=t=>b.value.docs=t),title:"Handler reference",icon:f(wt),size:"lg"},{footer:g(()=>[d(M,{variant:"secondary",onClick:e[44]||(e[44]=t=>b.value.docs=!1)},{default:g(()=>[...e[117]||(e[117]=[u(" Close ",-1)])]),_:1})]),default:g(()=>[o("div",Fr,[e[115]||(e[115]=o("p",null,[u(" Export a single "),o("code",{class:"font-mono text-white"},"handler(event)"),u(" that returns an HTTP-shaped object. Orva injects env vars and secrets at spawn time. ")],-1)),o("pre",Hr,p(Lt.value),1),e[116]||(e[116]=o("ul",{class:"space-y-1 pl-4 list-disc marker:text-foreground-muted/50"},[o("li",null,[o("span",{class:"text-white font-mono"},"event.body"),u(" is the raw request body (string or parsed JSON).")]),o("li",null,[u("Return "),o("span",{class:"text-white font-mono"},"{ statusCode, headers, body }"),u(".")]),o("li",null,[u("Add packages via the "),o("span",{class:"text-white"},"Deps"),u(" panel (installed at build time.")])],-1)),d(a,{to:"/docs",class:"inline-flex items-center gap-1 text-foreground-muted hover:text-white transition-colors",onClick:e[43]||(e[43]=t=>b.value.docs=!1)},{default:g(()=>[...e[114]||(e[114]=[u(" Open full docs in this UI → ",-1)])]),_:1})])]),_:1},8,["modelValue","icon"]),d(re,{modelValue:b.value.firstDeploy,"onUpdate:modelValue":e[50]||(e[50]=t=>b.value.firstDeploy=t),title:"Name & deploy",icon:f(qe),size:"md"},{footer:g(()=>[d(M,{variant:"secondary",onClick:e[49]||(e[49]=t=>b.value.firstDeploy=!1)},{default:g(()=>[...e[121]||(e[121]=[u(" Cancel ",-1)])]),_:1}),d(M,{disabled:!n.value.name.trim(),loading:Y.value,onClick:ft},{default:g(()=>[d(f(qe),{class:"w-4 h-4"}),e[122]||(e[122]=u(" Deploy ",-1))]),_:1},8,["disabled","loading"])]),default:g(()=>[o("div",Gr,[o("div",null,[e[118]||(e[118]=o("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"}," Function name ",-1)),o("div",Jr,[_(o("input",{ref_key:"firstDeployNameInput",ref:We,"onUpdate:modelValue":e[46]||(e[46]=t=>n.value.name=t),placeholder:"my-function",class:"w-full bg-background border border-border rounded-md pl-3 pr-10 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onKeydown:No(ft,["enter"])},null,544),[[O,n.value.name]]),o("button",{type:"button",class:"absolute right-1.5 top-1/2 -translate-y-1/2 p-1.5 rounded text-foreground-muted hover:text-white hover:bg-surface-hover transition-colors",title:"Re-roll a fresh name",onClick:mt},[d(f(Tt),{class:"w-3.5 h-3.5"})])]),e[119]||(e[119]=o("p",{class:"text-[11px] text-foreground-muted mt-1.5"}," Lowercase, dash-separated. Used in the invoke URL. Re-roll for a different combination. ",-1))]),o("div",null,[o("label",Kr,[e[120]||(e[120]=o("span",null,"Runtime",-1)),fe.value&&!pe.value?(i(),l("span",Wr,"auto-detected")):h("",!0)]),o("div",Yr,[(i(),l(S,null,C(rt,t=>o("button",{key:t.id,class:oe(["px-2 py-2 rounded border text-xs font-medium transition-colors duration-150 flex items-center justify-center",n.value.runtime===t.id?"bg-white text-black border-white":"bg-surface-hover text-foreground-muted border-border hover:border-foreground-muted"]),onClick:r=>lt(t.id)},p(t.label),11,Xr)),64))])]),o("div",Qr,[d(de,{modelValue:n.value.memory_mb,"onUpdate:modelValue":e[47]||(e[47]=t=>n.value.memory_mb=t),modelModifiers:{number:!0},label:"Memory (MB)",type:"number",placeholder:"64"},null,8,["modelValue"]),d(de,{modelValue:n.value.cpus,"onUpdate:modelValue":e[48]||(e[48]=t=>n.value.cpus=t),modelModifiers:{number:!0},label:"CPUs",type:"number",placeholder:"0.5"},null,8,["modelValue"])])])]),_:1},8,["modelValue","icon"])])}}},wa=po(Zr,[["__scopeId","data-v-c85cd9a2"]]);export{wa as default}; +${c}`,danger:!0}):w.notify({title:"Rollback failed",message:c,danger:!0})}finally{Te.value=!1}}},Ee=async()=>{if(m.value)try{const s=await T.get(`/functions/${m.value}/secrets`);je.value=(s.data.secrets||[]).map(e=>({id:e,name:e}))}catch(s){console.error("Failed to load secrets",s)}},io=async()=>{const s=P.value.name.trim();if(s){if(!m.value){H.value=H.value.filter(e=>e.name!==s),H.value.push({name:s,value:P.value.value}),P.value.name="",P.value.value="";return}try{await T.post(`/functions/${m.value}/secrets`,{key:s,value:P.value.value}),P.value.name="",P.value.value="",await Ee()}catch(e){y.value=e.response?.data?.error?.message||"Failed to save secret"}}},lo=s=>{H.value.splice(s,1)},uo=async s=>{if(!(!m.value||!await w.ask({title:"Delete secret?",message:`"${s}" will be removed from this function's environment.`,confirmLabel:"Delete",danger:!0})))try{await T.delete(`/functions/${m.value}/secrets/${encodeURIComponent(s)}`),await Ee()}catch(a){y.value=a.response?.data?.error?.message||"Failed to delete secret"}},co=async()=>{if(!(!m.value||!H.value.length)){for(const s of H.value)try{await T.post(`/functions/${m.value}/secrets`,{key:s.name,value:s.value}),x.value.push(`Saved secret: ${s.name}`)}catch(e){const a=e.response?.data?.error?.message||e.message;x.value.push(`Failed to save secret ${s.name}: ${a}`)}H.value=[],await Ee()}},mo=async()=>{await w.ask({title:"Reset editor?",message:"Unsaved changes in the editor will be discarded.",confirmLabel:"Reset",danger:!0})&&(n.value.name="",m.value="",k.value="",ce.value=!1,A.value=null,y.value=null,Pe("python314"))};return(s,e)=>{const a=yo("router-link");return i(),l("div",ws,[o("h1",ks,p(n.value.name||"New function"),1),o("div",Ss,[o("div",Cs,[d(f(bt),{class:"w-4 h-4 text-foreground-muted shrink-0"}),_(o("input",{"onUpdate:modelValue":e[0]||(e[0]=t=>n.value.name=t),placeholder:"my-function",disabled:R.value,class:"bg-transparent border-0 text-base sm:text-sm font-medium text-white placeholder-foreground-muted focus:outline-none px-1 py-1 min-w-0 flex-1 sm:flex-none sm:w-40"},null,8,Ts),[[O,n.value.name]]),!R.value&&!m.value?(i(),l("button",{key:0,type:"button",class:"p-1 rounded text-foreground-muted hover:text-white hover:bg-surface-hover transition-colors shrink-0 touch-expand-iconbtn",title:"Re-roll a fresh name","aria-label":"Re-roll a fresh name",onClick:mt},[d(f(Tt),{class:"w-3.5 h-3.5"})])):h("",!0),o("span",Es,p(Pt(n.value.runtime)),1)]),o("div",Os,[o("div",{class:"relative",ref_key:"configMenuRef",ref:Ye},[o("button",{type:"button",class:"panel-btn","aria-haspopup":"menu","aria-expanded":j.value.config,onClick:e[1]||(e[1]=t=>Qe("config"))},[d(f(Ue),{class:"w-3.5 h-3.5"}),e[51]||(e[51]=u(" Config ",-1)),d(f(be),{class:"w-3 h-3 text-foreground-muted"})],8,Rs),j.value.config?(i(),l("div",js,[o("button",{class:"menu-item",role:"menuitem",onClick:e[2]||(e[2]=t=>ae("settings"))},[d(f(Ue),{class:"w-3.5 h-3.5"}),e[52]||(e[52]=o("span",{class:"flex-1 text-left"},"Settings",-1)),e[53]||(e[53]=o("span",{class:"text-[10px] text-foreground-muted"},"runtime · limits",-1))]),o("button",{class:"menu-item",role:"menuitem",onClick:e[3]||(e[3]=t=>ae("envVars"))},[d(f(xt),{class:"w-3.5 h-3.5"}),e[54]||(e[54]=o("span",{class:"flex-1 text-left"},"Env",-1)),ot.value?(i(),l("span",Ns,p(ot.value),1)):h("",!0)]),o("button",{class:"menu-item",role:"menuitem",onClick:e[4]||(e[4]=t=>ae("deps"))},[d(f(yt),{class:"w-3.5 h-3.5"}),e[55]||(e[55]=o("span",{class:"flex-1 text-left"},"Deps",-1)),e[56]||(e[56]=o("span",{class:"text-[10px] text-foreground-muted"},"package · requirements",-1))]),o("button",{class:"menu-item",role:"menuitem",onClick:e[5]||(e[5]=t=>ae("secrets"))},[d(f(_t),{class:"w-3.5 h-3.5"}),e[57]||(e[57]=o("span",{class:"flex-1 text-left"},"Secrets",-1)),Ae.value?(i(),l("span",Ds,p(Ae.value),1)):h("",!0)]),R.value&&ie.value.length?(i(),l("button",{key:0,class:"menu-item",role:"menuitem",onClick:e[6]||(e[6]=t=>ae("versions"))},[d(f(Ct),{class:"w-3.5 h-3.5"}),e[58]||(e[58]=o("span",{class:"flex-1 text-left"},"Versions",-1)),o("span",As,p(ie.value.length),1)])):h("",!0)])):h("",!0)],512),o("div",{class:"relative",ref_key:"bindingsMenuRef",ref:Xe},[o("button",{type:"button",class:"panel-btn","aria-haspopup":"menu","aria-expanded":j.value.bindings,onClick:e[7]||(e[7]=t=>Qe("bindings"))},[d(f(ho),{class:"w-3.5 h-3.5"}),e[59]||(e[59]=u(" Bindings ",-1)),d(f(be),{class:"w-3 h-3 text-foreground-muted"})],8,Ps),j.value.bindings?(i(),l("div",Is,[R.value?(i(),l("button",{key:0,class:"menu-item",role:"menuitem",onClick:e[8]||(e[8]=t=>Ze({name:"function-kv",params:{name:n.value.name}}))},[d(f(Ko),{class:"w-3.5 h-3.5"}),e[60]||(e[60]=o("span",{class:"flex-1 text-left"},"KV store",-1)),e[61]||(e[61]=o("span",{class:"text-[10px] text-foreground-muted"},"per-function state",-1))])):h("",!0),R.value?(i(),l("button",{key:1,class:"menu-item",role:"menuitem",onClick:e[9]||(e[9]=t=>Ze({name:"function-inbound-webhooks",params:{name:n.value.name}}))},[d(f(go),{class:"w-3.5 h-3.5"}),e[62]||(e[62]=o("span",{class:"flex-1 text-left"},"Inbound webhooks",-1)),e[63]||(e[63]=o("span",{class:"text-[10px] text-foreground-muted"},"signed POST",-1))])):h("",!0),o("button",{class:"menu-item",role:"menuitem",onClick:e[10]||(e[10]=t=>ae("docs"))},[d(f(wt),{class:"w-3.5 h-3.5"}),e[64]||(e[64]=o("span",{class:"flex-1 text-left"},"Docs",-1)),e[65]||(e[65]=o("span",{class:"text-[10px] text-foreground-muted"},"handler reference",-1))])])):h("",!0)],512),e[67]||(e[67]=o("div",{class:"w-px h-5 bg-border mx-1"},null,-1)),d(V,{variant:"secondary",size:"sm",onClick:mo},{default:g(()=>[...e[66]||(e[66]=[u(" Reset ",-1)])]),_:1}),d(V,{size:"sm",loading:Y.value,onClick:pt},{default:g(()=>[d(f(qe),{class:"w-4 h-4"}),u(" "+p(R.value?"Deploy New Version":"Deploy"),1)]),_:1},8,["loading"])])]),m.value?(i(),l("div",Vs,[e[69]||(e[69]=o("span",{class:"text-foreground-muted shrink-0 uppercase tracking-wider text-[10px]"},"Invoke URL",-1)),o("code",Ms,p(ke.value),1),o("button",{class:"px-2 py-1 rounded text-foreground-muted hover:text-white hover:bg-surface-hover transition-colors flex items-center gap-1 shrink-0",onClick:Vt},[_e.value?(i(),le(f(Uo),{key:0,class:"w-3 h-3 text-success"})):(i(),le(f(qo),{key:1,class:"w-3 h-3"})),u(" "+p(_e.value?"Copied":"Copy"),1)]),R.value&&n.value.name?(i(),le(a,{key:0,to:`/functions/${n.value.name}/deployments`,class:"text-foreground-muted hover:text-white transition-colors px-2"},{default:g(()=>[...e[68]||(e[68]=[u(" Deployments → ",-1)])]),_:1},8,["to"])):h("",!0)])):h("",!0),o("div",Ls,[o("div",$s,[o("div",Us,[d(f(bt),{class:"w-3 h-3"}),o("span",qs,p(it.value),1),Z.value?(i(),l("span",Bs,"· template: "+p(Z.value),1)):h("",!0)]),o("div",zs,p(k.value.length)+" chars ",1)]),d(f(Dt),{modelValue:k.value,"onUpdate:modelValue":e[11]||(e[11]=t=>k.value=t),language:n.value.runtime,class:"flex-1 min-h-0"},null,8,["modelValue","language"])]),o("div",Fs,[o("div",Hs,[(i(!0),l(S,null,C(At.value,t=>(i(),l("button",{key:t.id,class:oe(["px-3 h-9 text-xs flex items-center gap-1.5 border-b-2 transition-colors",B.value===t.id?"text-white border-primary":"text-foreground-muted border-transparent hover:text-white"]),onClick:r=>{B.value=t.id,q.value=!0}},[(i(),le(Oo(t.icon),{class:"w-3 h-3"})),u(" "+p(t.label)+" ",1),t.badge?(i(),l("span",Js,p(t.badge),1)):h("",!0)],10,Gs))),128)),o("div",Ks,[B.value==="test"?(i(),l("button",{key:0,disabled:!$.value||ye.value,class:"run-btn",title:$.value?"Invoke with the payload below":"Deploy first",onClick:Qt},[ye.value?(i(),l("span",Ys)):(i(),le(f(kt),{key:0,class:"w-3 h-3"})),e[70]||(e[70]=u(" Run ",-1))],8,Ws)):h("",!0),o("button",{class:"p-1.5 rounded text-foreground-muted hover:text-white hover:bg-surface-hover transition-colors",title:q.value?"Collapse":"Expand",onClick:e[12]||(e[12]=t=>q.value=!q.value)},[d(f(be),{class:oe(["w-4 h-4 transition-transform",q.value?"rotate-0":"rotate-180"])},null,8,["class"])],8,Xs)])]),_(o("div",Qs,[B.value==="build"?(i(),l("div",Zs,[x.value.length?h("",!0):(i(),l("div",en," No build activity yet. Deploy the function to stream logs here. ")),(i(!0),l(S,null,C(x.value,(t,r)=>(i(),l("div",{key:r,class:"text-foreground-muted whitespace-pre-wrap break-words"},p(t),1))),128))])):B.value==="test"?(i(),l("div",tn,[o("div",on,[o("div",sn,[_(o("select",{"onUpdate:modelValue":e[13]||(e[13]=t=>z.value=t),disabled:!$.value,class:"text-[11px] font-mono bg-background border border-border rounded px-1.5 py-0.5 text-foreground focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50"},[(i(),l(S,null,C(It,t=>o("option",{key:t,value:t},p(t),9,rn)),64))],8,nn),[[Oe,z.value]]),_(o("input",{"onUpdate:modelValue":e[14]||(e[14]=t=>J.value=t),disabled:!$.value,spellcheck:"false",placeholder:"/",class:"flex-1 min-w-0 text-[11px] font-mono bg-background border border-border rounded px-2 py-0.5 text-foreground focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50"},null,8,an),[[O,J.value]]),o("div",ln,[o("button",{type:"button",class:"text-[11px] font-medium text-foreground-muted hover:text-white px-1.5 py-0.5 rounded hover:bg-surface-hover transition-colors flex items-center gap-1",disabled:!m.value,title:m.value?"Saved fixtures for this function":"Deploy first",onClick:oo},[e[71]||(e[71]=u(" Saved ",-1)),W.value.length?(i(),l("span",un,"· "+p(W.value.length),1)):h("",!0),d(f(be),{class:"w-3 h-3"})],8,dn),se.value?(i(),l("div",{key:0,class:"absolute right-0 top-full mt-1 z-30 w-64 bg-surface border border-border rounded shadow-lg overflow-hidden",onMouseleave:e[15]||(e[15]=t=>se.value=!1)},[e[72]||(e[72]=o("div",{class:"px-3 py-2 text-[10px] uppercase tracking-[0.14em] text-foreground-muted/80 border-b border-border bg-surface/60"}," Saved fixtures ",-1)),W.value.length?(i(),l("ul",mn,[(i(!0),l(S,null,C(W.value,t=>(i(),l("li",{key:t.id,class:"flex items-center gap-2 px-3 py-1.5 text-xs hover:bg-surface-hover cursor-pointer group",onClick:r=>so(t)},[o("span",fn,p(t.method),1),o("span",vn,p(t.name),1),o("button",{type:"button",class:"opacity-100 lg:opacity-0 lg:group-hover:opacity-100 text-foreground-muted hover:text-red-400 transition-opacity",title:`Delete ${t.name}`,"aria-label":`Delete ${t.name}`,onClick:Ro(r=>no(t),["stop"])},[d(f(Be),{class:"w-3 h-3"})],8,hn)],8,pn))),128))])):(i(),l("div",cn," No fixtures yet. Set up a request and click Save. ")),o("div",gn,[o("button",{type:"button",class:"text-[11px] text-foreground hover:text-white w-full text-left px-1.5 py-1 rounded hover:bg-surface-hover transition-colors disabled:opacity-50",disabled:!$.value,onClick:ro}," + Save current as… ",8,bn)])],32)):h("",!0)])]),o("div",yn,[o("button",{type:"button",class:"w-full h-6 px-3 flex items-center justify-between text-[10px] uppercase tracking-[0.14em] text-foreground-muted hover:text-white bg-surface/30 transition-colors",onClick:e[16]||(e[16]=t=>K.value=!K.value)},[o("span",null,[e[73]||(e[73]=u(" Headers ",-1)),st.value?(i(),l("span",xn,"· "+p(st.value),1)):h("",!0)]),d(f(be),{class:oe(["w-3 h-3 transition-transform",K.value?"rotate-0":"-rotate-90"])},null,8,["class"])]),K.value?(i(),l("div",_n,[(i(!0),l(S,null,C(D.value,(t,r)=>(i(),l("div",{key:r,class:"flex items-center gap-1.5"},[_(o("input",{"onUpdate:modelValue":c=>t.name=c,disabled:!$.value,spellcheck:"false",placeholder:"Header name",class:"flex-1 min-w-0 text-[11px] font-mono bg-background border border-border rounded px-2 py-0.5 text-foreground focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50"},null,8,wn),[[O,t.name]]),_(o("input",{"onUpdate:modelValue":c=>t.value=c,disabled:!$.value,spellcheck:"false",placeholder:"value",class:"flex-1 min-w-0 text-[11px] font-mono bg-background border border-border rounded px-2 py-0.5 text-foreground focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-50"},null,8,kn),[[O,t.value]]),o("button",{type:"button",class:"text-foreground-muted hover:text-red-400 p-0.5 transition-colors",title:"Remove header",onClick:c=>to(r)},[d(f($e),{class:"w-3 h-3"})],8,Sn)]))),128)),o("button",{type:"button",class:"text-[11px] text-foreground-muted hover:text-white transition-colors px-1.5 py-0.5 rounded hover:bg-surface-hover",disabled:!$.value,onClick:eo}," + Add header ",8,Cn)])):h("",!0)]),o("div",Tn,[e[74]||(e[74]=o("span",{class:"text-[10px] uppercase tracking-[0.14em] font-medium text-foreground-muted"}," Body ",-1)),$.value?(i(),l("span",On,p(M.value.length)+" chars",1)):(i(),l("span",En,"Deploy first"))]),_(o("textarea",{"onUpdate:modelValue":e[17]||(e[17]=t=>M.value=t),disabled:!$.value,spellcheck:"false",class:"flex-1 w-full min-h-0 bg-background text-xs font-mono p-3 text-foreground focus:outline-none resize-none disabled:opacity-50 placeholder:text-foreground-muted/50",placeholder:"{}"},null,8,Rn),[[O,M.value]])]),o("div",jn,[o("div",Nn,[o("span",{class:oe(["text-[10px] uppercase tracking-[0.14em] font-medium flex items-center gap-1.5",y.value?"text-red-400":A.value?"text-success":"text-foreground-muted"])},[o("span",{class:oe(["w-1.5 h-1.5 rounded-full",y.value?"bg-red-400":A.value?"bg-success":"bg-foreground-muted/40"])},null,2),u(" "+p(y.value?"Error":A.value?"Response":"Idle"),1)],2),o("div",Dn,[me.value?(i(),l("button",{key:0,type:"button",class:"text-[10px] uppercase tracking-[0.14em] text-foreground-muted hover:text-white px-1.5 py-0.5 rounded hover:bg-surface-hover transition-colors flex items-center gap-1 disabled:opacity-50",disabled:we.value,title:"Build a paste-ready debug prompt with source + request + stderr",onClick:Zt},[d(f(Bo),{class:"w-3 h-3"}),e[75]||(e[75]=u(" Suggest fix ",-1))],8,An)):h("",!0),xe.value?(i(),l("span",Pn,p(X.value)+" · "+p(xe.value)+"ms",1)):h("",!0)])]),o("div",In,[A.value||y.value?(i(),l("pre",{key:0,class:oe(["px-3 py-2.5 font-mono text-xs whitespace-pre-wrap break-all leading-relaxed",y.value?"text-red-200":"text-foreground"])},p(A.value||y.value),3)):(i(),l("div",Vn,[...e[76]||(e[76]=[u(" Hit ",-1),o("span",{class:"not-italic text-white"},"Run",-1),u(" to invoke this function with the request payload. ",-1)])])),ne.value.length?(i(),l("div",Mn,[o("div",Ln," Function logs · "+p(ne.value.length),1),o("div",$n,[(i(!0),l(S,null,C(ne.value,(t,r)=>(i(),l("div",{key:r,class:"text-foreground-muted whitespace-pre-wrap break-words"},p(t),1))),128))])])):h("",!0)])])])):h("",!0)],512),[[bo,q.value]])]),d(re,{modelValue:b.value.settings,"onUpdate:modelValue":e[30]||(e[30]=t=>b.value.settings=t),title:"Function configuration",icon:f(Ue),size:"md"},{footer:g(()=>[d(V,{variant:"secondary",onClick:e[29]||(e[29]=t=>b.value.settings=!1)},{default:g(()=>[...e[103]||(e[103]=[u(" Done ",-1)])]),_:1})]),default:g(()=>[o("div",Un,[o("div",null,[e[77]||(e[77]=o("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"Description",-1)),_(o("textarea",{"onUpdate:modelValue":e[18]||(e[18]=t=>n.value.description=t),rows:"2",placeholder:"One-line summary of what this function does. Surfaces in MCP tool catalogs and the agent channel picker.",class:"w-full bg-surface-hover border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white resize-y"},null,512),[[O,n.value.description]])]),o("div",null,[o("label",qn,[e[78]||(e[78]=o("span",null,"Runtime",-1)),fe.value&&!pe.value?(i(),l("span",Bn,"auto-detected")):h("",!0)]),o("div",zn,[(i(),l(S,null,C(rt,t=>o("button",{key:t.id,class:oe(["px-2 py-2 rounded border text-xs font-medium transition-colors duration-150 flex items-center justify-center",n.value.runtime===t.id?"bg-white text-black border-white":"bg-surface-hover text-foreground-muted border-border hover:border-foreground-muted"]),onClick:r=>lt(t.id)},p(t.label),11,Fn)),64))])]),o("div",null,[e[80]||(e[80]=o("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"Template",-1)),_(o("select",{"onUpdate:modelValue":e[19]||(e[19]=t=>Z.value=t),class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onChange:Bt},[e[79]||(e[79]=o("option",{value:""}," Custom (blank) ",-1)),(i(!0),l(S,null,C(zt.value,t=>(i(),l("optgroup",{key:t.label,label:t.label},[(i(!0),l(S,null,C(t.items,r=>(i(),l("option",{key:r.id,value:r.id},p(r.label)+p(r.cron?" · scheduled":"")+": "+p(r.description),9,Gn))),128))],8,Hn))),128))],544),[[Oe,Z.value]]),dt.value?(i(),l("p",Jn,p(dt.value),1)):h("",!0)]),o("div",Kn,[d(de,{modelValue:n.value.memory_mb,"onUpdate:modelValue":e[20]||(e[20]=t=>n.value.memory_mb=t),modelModifiers:{number:!0},label:"Memory (MB)",type:"number",placeholder:"64"},null,8,["modelValue"]),d(de,{modelValue:n.value.cpus,"onUpdate:modelValue":e[21]||(e[21]=t=>n.value.cpus=t),modelModifiers:{number:!0},label:"CPUs",type:"number",placeholder:"0.5"},null,8,["modelValue"])]),o("div",Wn,[e[83]||(e[83]=o("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block"},"Concurrency",-1)),o("div",Yn,[o("div",null,[d(de,{modelValue:n.value.max_concurrency,"onUpdate:modelValue":e[22]||(e[22]=t=>n.value.max_concurrency=t),modelModifiers:{number:!0},label:"Max concurrent (0 = unlimited)",type:"number",placeholder:"0"},null,8,["modelValue"])]),o("div",null,[e[82]||(e[82]=o("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"},"When at cap",-1)),_(o("select",{"onUpdate:modelValue":e[23]||(e[23]=t=>n.value.concurrency_policy=t),disabled:!n.value.max_concurrency,class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white disabled:opacity-50"},[...e[81]||(e[81]=[o("option",{value:"queue"}," Queue requests ",-1),o("option",{value:"reject"}," Reject (429) ",-1)])],8,Xn),[[Oe,n.value.concurrency_policy]])])]),e[84]||(e[84]=o("p",{class:"text-[11px] text-foreground-muted leading-snug"}," Caps how many in-flight invocations one function can have. Use this to protect downstream APIs from a runaway handler. ",-1))]),o("div",Qn,[o("label",Zn,[_(o("input",{"onUpdate:modelValue":e[24]||(e[24]=t=>n.value.network_mode=t),type:"checkbox","true-value":"egress","false-value":"none",class:"mt-0.5 w-4 h-4 rounded border-border bg-background focus:outline-none focus:ring-1 focus:ring-white"},null,512),[[jo,n.value.network_mode]]),o("div",er,[o("div",tr,[d(f(St),{class:"w-4 h-4 text-foreground-muted"}),e[85]||(e[85]=u(" Allow outbound network ",-1))]),e[86]||(e[86]=o("div",{class:"text-xs text-foreground-muted mt-1 leading-snug"}," Off by default. Turn on if this function needs to call external APIs (Stripe, OpenAI, your DB). Adds ~5 ms cold-start. ",-1))])])]),o("div",or,[o("label",sr,[d(f(Fo),{class:"w-3.5 h-3.5"}),e[87]||(e[87]=u(" Invoke gate ",-1))]),_(o("select",{"onUpdate:modelValue":e[25]||(e[25]=t=>n.value.auth_mode=t),class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white"},[...e[88]||(e[88]=[o("option",{value:"none"}," Public, anyone can invoke ",-1),o("option",{value:"platform_key"}," Require Orva API key (server-to-server) ",-1),o("option",{value:"signed"}," Require HMAC signature (X-Orva-Signature) ",-1)])],512),[[Oe,n.value.auth_mode]]),e[89]||(e[89]=o("p",{class:"text-[11px] text-foreground-muted leading-snug"},[u(" Public is the default, matches Cloudflare Workers and Vercel Functions. For end-user auth (JWT, session cookies), keep this on "),o("span",{class:"text-white"},"Public"),u(" and verify inside your handler. "),o("span",{class:"text-white"},"Signed"),u(" mode reads its key from the function secret "),o("span",{class:"font-mono"},"ORVA_SIGNING_SECRET"),u(". ")],-1))]),o("div",nr,[e[90]||(e[90]=o("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block"},"Rate limit",-1)),d(de,{modelValue:n.value.rate_limit_per_min,"onUpdate:modelValue":e[26]||(e[26]=t=>n.value.rate_limit_per_min=t),modelModifiers:{number:!0},label:"Requests per minute, per IP (0 = unlimited)",type:"number",placeholder:"0"},null,8,["modelValue"]),e[91]||(e[91]=o("p",{class:"text-[11px] text-foreground-muted leading-snug"},[u(" Token-bucket per client IP. A burst up to the limit is allowed, then refills at rate/60 per second. Returns 429 with "),o("span",{class:"font-mono"},"Retry-After: 60"),u(" when exceeded. ")],-1))]),o("div",rr,[o("label",ar,[d(f(St),{class:"w-3.5 h-3.5"}),e[92]||(e[92]=u(" Custom routes ",-1)),De.value?(i(),l("span",ir,"loading…")):h("",!0)]),m.value?nt.value.length===0?(i(),l("div",dr,[e[93]||(e[93]=u(" No custom routes for this function. Default invoke URL is ",-1)),o("span",ur,"/fn/"+p(m.value.slice(0,8))+"…",1),e[94]||(e[94]=u(". Add a pretty path below (e.g. ",-1)),e[95]||(e[95]=o("span",{class:"font-mono"},"/webhooks/stripe",-1)),e[96]||(e[96]=u(" or ",-1)),e[97]||(e[97]=o("span",{class:"font-mono"},"/api/payments/*",-1)),e[98]||(e[98]=u(" for prefix match). ",-1))])):(i(),l("ul",cr,[(i(!0),l(S,null,C(nt.value,t=>(i(),l("li",{key:t.path,class:"flex items-center gap-2 px-2.5 py-1.5 rounded border border-border bg-surface-hover/50"},[o("code",mr,p(t.path),1),t.methods&&t.methods!=="*"?(i(),l("span",pr,p(t.methods),1)):h("",!0),o("button",{class:"shrink-0 w-6 h-6 flex items-center justify-center rounded text-foreground-muted hover:text-red-400 hover:bg-surface transition-colors",title:"Remove route",onClick:r=>Jt(t.path)},[d(f($e),{class:"w-3 h-3"})],8,fr)]))),128))])):(i(),l("p",lr," Save the function first. Custom routes need a target function id. ")),m.value?(i(),l("div",vr,[o("div",hr,[_(o("input",{"onUpdate:modelValue":e[27]||(e[27]=t=>ee.value.path=t),placeholder:"/path or /prefix/*",class:"flex-1 min-w-0 bg-background border border-border rounded-md px-2 py-1.5 text-xs font-mono text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white"},null,512),[[O,ee.value.path]]),_(o("input",{"onUpdate:modelValue":e[28]||(e[28]=t=>ee.value.methods=t),placeholder:"*",title:"Comma-separated methods or * for any (default *)",class:"w-20 bg-background border border-border rounded-md px-2 py-1.5 text-xs font-mono text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white"},null,512),[[O,ee.value.methods]]),o("button",{class:"shrink-0 px-3 py-1.5 rounded-md bg-white text-black text-xs font-medium hover:bg-white/90 transition-colors",onClick:Gt}," Add ")]),L.value?(i(),l("p",gr,[e[99]||(e[99]=u(" ⚠ ",-1)),o("span",br,p(L.value.path),1),e[100]||(e[100]=u(" already maps to function ",-1)),o("span",yr,p(L.value.currentFunctionId.slice(0,8))+"…",1),e[101]||(e[101]=u("; clicking Add will remap it to this one. ",-1))])):h("",!0),I.value?(i(),l("p",xr,p(I.value),1)):h("",!0),e[102]||(e[102]=o("p",{class:"text-[11px] text-foreground-muted leading-snug"},[u(" Reserved prefixes: "),o("span",{class:"font-mono"},"/api/"),u(", "),o("span",{class:"font-mono"},"/auth/"),u(", "),o("span",{class:"font-mono"},"/web/"),u(", "),o("span",{class:"font-mono"},"/_orva/"),u(". Prefix routes must end in "),o("span",{class:"font-mono"},"/*"),u(". ")],-1))])):h("",!0)])])]),_:1},8,["modelValue","icon"]),d(re,{modelValue:b.value.envVars,"onUpdate:modelValue":e[32]||(e[32]=t=>b.value.envVars=t),title:"Environment variables",icon:f(xt),size:"md"},{footer:g(()=>[d(V,{variant:"secondary",onClick:e[31]||(e[31]=t=>b.value.envVars=!1)},{default:g(()=>[...e[105]||(e[105]=[u(" Done ",-1)])]),_:1})]),default:g(()=>[o("div",_r,[(i(!0),l(S,null,C(F.value,(t,r)=>(i(),l("div",{key:r,class:"flex items-center gap-2"},[_(o("input",{"onUpdate:modelValue":c=>t.key=c,placeholder:"KEY",class:"flex-1 min-w-0 bg-background border border-border rounded-md px-2 py-1.5 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white"},null,8,wr),[[O,t.key]]),_(o("input",{"onUpdate:modelValue":c=>t.value=c,placeholder:"VALUE",class:"flex-1 min-w-0 bg-background border border-border rounded-md px-2 py-1.5 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white"},null,8,kr),[[O,t.value]]),o("button",{class:"shrink-0 w-7 h-7 flex items-center justify-center rounded text-foreground-muted hover:text-red-400 hover:bg-surface transition-colors",title:"Remove",onClick:c=>Ht(r)},[d(f($e),{class:"w-3.5 h-3.5"})],8,Sr)]))),128)),o("button",{class:"text-xs text-foreground-muted hover:text-white transition-colors",onClick:Ft}," + Add variable "),e[104]||(e[104]=o("p",{class:"text-[11px] text-foreground-muted pt-2 border-t border-border"},[u(" Plaintext at deploy time. Use "),o("span",{class:"text-white"},"Secrets"),u(" for sensitive values. ")],-1))])]),_:1},8,["modelValue","icon"]),d(re,{modelValue:b.value.deps,"onUpdate:modelValue":e[35]||(e[35]=t=>b.value.deps=t),title:"Dependencies",icon:f(yt),size:"md"},{footer:g(()=>[d(V,{variant:"secondary",onClick:e[34]||(e[34]=t=>b.value.deps=!1)},{default:g(()=>[...e[106]||(e[106]=[u(" Done ",-1)])]),_:1})]),default:g(()=>[o("div",Cr,[o("div",Tr,p($t.value),1),_(o("textarea",{"onUpdate:modelValue":e[33]||(e[33]=t=>Q.value=t),class:"w-full bg-surface-hover border border-border rounded-md text-xs font-mono p-3 text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white resize-none min-h-[200px]",placeholder:"One package per line. e.g. requests==2.31.0"},null,512),[[O,Q.value]])])]),_:1},8,["modelValue","icon"]),d(re,{modelValue:b.value.secrets,"onUpdate:modelValue":e[39]||(e[39]=t=>b.value.secrets=t),title:"Secrets",icon:f(_t),size:"md"},{footer:g(()=>[d(V,{variant:"secondary",onClick:e[38]||(e[38]=t=>b.value.secrets=!1)},{default:g(()=>[...e[109]||(e[109]=[u(" Done ",-1)])]),_:1})]),default:g(()=>[o("div",Er,[Ae.value?h("",!0):(i(),l("div",Or,[e[107]||(e[107]=u(" No secrets yet. Add a key-value pair below.",-1)),m.value?h("",!0):(i(),l("span",Rr," They'll be saved when you deploy."))])),(i(!0),l(S,null,C(je.value,t=>(i(),l("div",{key:t.id,class:"flex items-center justify-between text-xs px-3 py-2 rounded border border-border"},[o("span",jr,p(t.name),1),o("button",{class:"text-foreground-muted hover:text-red-400 transition-colors",onClick:r=>uo(t.id)},[d(f(Be),{class:"w-3.5 h-3.5"})],8,Nr)]))),128)),(i(!0),l(S,null,C(H.value,(t,r)=>(i(),l("div",{key:"pending-"+r,class:"flex items-center justify-between text-xs px-3 py-2 rounded border border-amber-500/30 bg-amber-500/5"},[o("div",Dr,[o("span",Ar,p(t.name),1),e[108]||(e[108]=o("span",{class:"text-[10px] uppercase tracking-wider text-amber-400/80"},"pending",-1))]),o("button",{class:"text-foreground-muted hover:text-red-400 transition-colors",onClick:c=>lo(r)},[d(f(Be),{class:"w-3.5 h-3.5"})],8,Pr)]))),128)),o("div",Ir,[_(o("input",{"onUpdate:modelValue":e[36]||(e[36]=t=>P.value.name=t),placeholder:"SECRET_NAME",class:"w-full bg-background border border-border rounded-md px-2 py-1.5 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white"},null,512),[[O,P.value.name]]),_(o("input",{"onUpdate:modelValue":e[37]||(e[37]=t=>P.value.value=t),placeholder:"SECRET_VALUE",type:"password",class:"w-full bg-background border border-border rounded-md px-2 py-1.5 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white"},null,512),[[O,P.value.value]]),d(V,{class:"w-full",variant:"secondary",onClick:io},{default:g(()=>[d(f(Ho),{class:"w-4 h-4"}),u(" "+p(m.value?"Save secret":"Queue secret for deploy"),1)]),_:1})])])]),_:1},8,["modelValue","icon"]),d(re,{modelValue:b.value.versions,"onUpdate:modelValue":e[42]||(e[42]=t=>b.value.versions=t),title:"Version history",icon:f(Ct),size:"md"},{footer:g(()=>[d(V,{variant:"secondary",onClick:e[41]||(e[41]=t=>b.value.versions=!1)},{default:g(()=>[...e[113]||(e[113]=[u(" Done ",-1)])]),_:1})]),default:g(()=>[o("div",Vr,[(i(!0),l(S,null,C(ie.value,t=>(i(),l("div",{key:t.deployment_id,class:"flex items-center justify-between gap-2 text-xs px-3 py-2 rounded border border-border"},[o("div",Mr,[o("span",Lr,"v"+p(t.version),1),t.is_active?(i(),l("span",$r,"Active")):h("",!0),o("span",{class:"font-mono text-foreground-muted truncate",title:t.code_hash},p(t.short_hash),9,Ur),e[110]||(e[110]=o("span",{class:"text-foreground-muted shrink-0"},"·",-1)),o("span",qr,p(new Date(t.created_at).toLocaleDateString()),1)]),t.is_active?h("",!0):(i(),l("div",Br,[ve.value?(i(),le(a,{key:0,to:{name:"function-diff",params:{name:f(U).params.name},query:{from:t.deployment_id,to:ve.value}},class:"text-foreground-muted hover:text-white flex items-center gap-1",title:"Compare with active version",onClick:e[40]||(e[40]=r=>b.value.versions=!1)},{default:g(()=>[d(f(zo),{class:"w-3 h-3"}),e[111]||(e[111]=u(" Compare ",-1))]),_:1},8,["to"])):h("",!0),o("button",{disabled:Te.value,class:"text-foreground-muted hover:text-white disabled:opacity-50 flex items-center gap-1",onClick:r=>ao(t)},[d(f(Go),{class:"w-3 h-3"}),e[112]||(e[112]=u(" Rollback ",-1))],8,zr)]))]))),128))])]),_:1},8,["modelValue","icon"]),d(re,{modelValue:b.value.docs,"onUpdate:modelValue":e[45]||(e[45]=t=>b.value.docs=t),title:"Handler reference",icon:f(wt),size:"lg"},{footer:g(()=>[d(V,{variant:"secondary",onClick:e[44]||(e[44]=t=>b.value.docs=!1)},{default:g(()=>[...e[117]||(e[117]=[u(" Close ",-1)])]),_:1})]),default:g(()=>[o("div",Fr,[e[115]||(e[115]=o("p",null,[u(" Export a single "),o("code",{class:"font-mono text-white"},"handler(event)"),u(" that returns an HTTP-shaped object. Orva injects env vars and secrets at spawn time. ")],-1)),o("pre",Hr,p(Lt.value),1),e[116]||(e[116]=o("ul",{class:"space-y-1 pl-4 list-disc marker:text-foreground-muted/50"},[o("li",null,[o("span",{class:"text-white font-mono"},"event.body"),u(" is the raw request body (string or parsed JSON).")]),o("li",null,[u("Return "),o("span",{class:"text-white font-mono"},"{ statusCode, headers, body }"),u(".")]),o("li",null,[u("Add packages via the "),o("span",{class:"text-white"},"Deps"),u(" panel (installed at build time.")])],-1)),d(a,{to:"/docs",class:"inline-flex items-center gap-1 text-foreground-muted hover:text-white transition-colors",onClick:e[43]||(e[43]=t=>b.value.docs=!1)},{default:g(()=>[...e[114]||(e[114]=[u(" Open full docs in this UI → ",-1)])]),_:1})])]),_:1},8,["modelValue","icon"]),d(re,{modelValue:b.value.firstDeploy,"onUpdate:modelValue":e[50]||(e[50]=t=>b.value.firstDeploy=t),title:"Name & deploy",icon:f(qe),size:"md"},{footer:g(()=>[d(V,{variant:"secondary",onClick:e[49]||(e[49]=t=>b.value.firstDeploy=!1)},{default:g(()=>[...e[121]||(e[121]=[u(" Cancel ",-1)])]),_:1}),d(V,{disabled:!n.value.name.trim(),loading:Y.value,onClick:ft},{default:g(()=>[d(f(qe),{class:"w-4 h-4"}),e[122]||(e[122]=u(" Deploy ",-1))]),_:1},8,["disabled","loading"])]),default:g(()=>[o("div",Gr,[o("div",null,[e[118]||(e[118]=o("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-1.5"}," Function name ",-1)),o("div",Jr,[_(o("input",{ref_key:"firstDeployNameInput",ref:We,"onUpdate:modelValue":e[46]||(e[46]=t=>n.value.name=t),placeholder:"my-function",class:"w-full bg-background border border-border rounded-md pl-3 pr-10 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onKeydown:No(ft,["enter"])},null,544),[[O,n.value.name]]),o("button",{type:"button",class:"absolute right-1.5 top-1/2 -translate-y-1/2 p-1.5 rounded text-foreground-muted hover:text-white hover:bg-surface-hover transition-colors",title:"Re-roll a fresh name",onClick:mt},[d(f(Tt),{class:"w-3.5 h-3.5"})])]),e[119]||(e[119]=o("p",{class:"text-[11px] text-foreground-muted mt-1.5"}," Lowercase, dash-separated. Used in the invoke URL. Re-roll for a different combination. ",-1))]),o("div",null,[o("label",Kr,[e[120]||(e[120]=o("span",null,"Runtime",-1)),fe.value&&!pe.value?(i(),l("span",Wr,"auto-detected")):h("",!0)]),o("div",Yr,[(i(),l(S,null,C(rt,t=>o("button",{key:t.id,class:oe(["px-2 py-2 rounded border text-xs font-medium transition-colors duration-150 flex items-center justify-center",n.value.runtime===t.id?"bg-white text-black border-white":"bg-surface-hover text-foreground-muted border-border hover:border-foreground-muted"]),onClick:r=>lt(t.id)},p(t.label),11,Xr)),64))])]),o("div",Qr,[d(de,{modelValue:n.value.memory_mb,"onUpdate:modelValue":e[47]||(e[47]=t=>n.value.memory_mb=t),modelModifiers:{number:!0},label:"Memory (MB)",type:"number",placeholder:"64"},null,8,["modelValue"]),d(de,{modelValue:n.value.cpus,"onUpdate:modelValue":e[48]||(e[48]=t=>n.value.cpus=t),modelModifiers:{number:!0},label:"CPUs",type:"number",placeholder:"0.5"},null,8,["modelValue"])])])]),_:1},8,["modelValue","icon"])])}}},Ca=po(Zr,[["__scopeId","data-v-4e8b616a"]]);export{Ca as default}; diff --git a/backend/internal/server/ui_dist/assets/Firewall-B1uYkmZh.js b/backend/internal/server/ui_dist/assets/Firewall-B1uYkmZh.js deleted file mode 100644 index 044af6c..0000000 --- a/backend/internal/server/ui_dist/assets/Firewall-B1uYkmZh.js +++ /dev/null @@ -1 +0,0 @@ -import{c as T,C as Ne,o as Te,E as Ve,a as i,b as l,d as n,h,_ as w,s as E,n as V,a2 as ae,t as v,g as Z,f as m,r as f,q as d,az as le,z as u,J as x,j as r,k as y,P as N,F as $,p as S,e as z,a5 as F,v as B,aZ as re}from"./index-WXhXpu06.js";import{_ as ze,a as oe,S as Be}from"./Modal--oERTKOu.js";import{R as Me}from"./refresh-cw-CEunRyai.js";import{G as ne}from"./globe-DI_Jf14z.js";import{T as je}from"./trash-2-dFLn8UYZ.js";const ie=T("asterisk",[["path",{d:"M12 6v12",key:"1vza4d"}],["path",{d:"M17.196 9 6.804 15",key:"1ah31z"}],["path",{d:"m6.804 9 10.392 6",key:"1b6pxd"}]]);const ue=T("earth",[["path",{d:"M21.54 15H17a2 2 0 0 0-2 2v4.54",key:"1djwo0"}],["path",{d:"M7 3.34V5a3 3 0 0 0 3 3a2 2 0 0 1 2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2c0-1.1.9-2 2-2h3.17",key:"1tzkfa"}],["path",{d:"M11 21.95V18a2 2 0 0 0-2-2a2 2 0 0 1-2-2v-1a2 2 0 0 0-2-2H2.05",key:"14pb5j"}],["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]]);const de=T("hash",[["line",{x1:"4",x2:"20",y1:"9",y2:"9",key:"4lhtct"}],["line",{x1:"4",x2:"20",y1:"15",y2:"15",key:"vyu0kd"}],["line",{x1:"10",x2:"8",y1:"3",y2:"21",key:"1ggp8o"}],["line",{x1:"16",x2:"14",y1:"3",y2:"21",key:"weycgp"}]]);const Oe=T("shield-alert",[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z",key:"oel41y"}],["path",{d:"M12 8v4",key:"1got3b"}],["path",{d:"M12 16h.01",key:"1drbdi"}]]);const Ue=T("shield-off",[["path",{d:"m2 2 20 20",key:"1ooewy"}],["path",{d:"M5 5a1 1 0 0 0-1 1v7c0 5 3.5 7.5 7.67 8.94a1 1 0 0 0 .67.01c2.35-.82 4.48-1.97 5.9-3.71",key:"1jlk70"}],["path",{d:"M9.309 3.652A12.252 12.252 0 0 0 11.24 2.28a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1v7a9.784 9.784 0 0 1-.08 1.264",key:"18rp1v"}]]),He={class:"space-y-8"},Le={class:"space-y-3"},Ee={class:"flex items-start justify-between gap-4 flex-wrap"},Ze={class:"flex items-center gap-2"},Fe={class:"flex-1 min-w-0"},We={key:0,class:"text-foreground-muted hidden sm:inline shrink-0"},qe={class:"dns-card"},Ge={class:"dns-row"},Ke={class:"dns-current"},Je={key:0,class:"dns-chips"},Ye={class:"font-mono"},Qe=["onClick"],Xe={key:1,class:"dns-defaults"},et={class:"font-mono"},tt={class:"dns-form"},st={class:"dns-row"},at={class:"dns-row-label"},lt={class:"dns-row-meta"},rt={key:0,class:"dns-records"},ot={class:"font-mono text-white text-xs flex-1 truncate"},nt={class:"font-mono text-foreground text-xs flex-1 truncate"},it=["onClick"],ut={key:1,class:"text-xs text-foreground-muted italic px-1"},dt={class:"dns-form"},ct={class:"dns-savebar"},vt={class:"rule-filterbar"},ft=["onClick"],mt={class:"rule-filter-count"},ht={key:0,class:"empty-card"},pt={class:"text-sm text-white"},yt={class:"text-xs text-foreground-muted mt-1 max-w-sm"},gt={key:1,class:"rule-grid"},bt={class:"space-y-4"},kt={class:"grid grid-cols-3 gap-2"},wt=["onClick"],xt={class:"text-[10.5px] text-foreground-muted mt-1.5 leading-snug"},Rt={__name:"Firewall",setup(_t){const p=Ne(),b=f([]),g=f({ipv4:[],ipv6:[],hostname_map:{},nftables_available:!0,last_error:""}),M=f(null),_=f(!1),j=f(!1),O=f(!1),c=f({rule_type:"cidr",value:"",label:""}),a=f({servers:[],search:"",records:[],defaults:[]}),P=f({servers:[],search:"",records:[]}),C=f(""),I=f(""),A=f(""),U=f(!1),ce=d(()=>{const e=JSON.stringify({s:a.value.servers||[],q:a.value.search||"",r:(a.value.records||[]).map(s=>`${s.host}=${s.ip}`).sort()}),t=JSON.stringify({s:P.value.servers||[],q:P.value.search||"",r:(P.value.records||[]).map(s=>`${s.host}=${s.ip}`).sort()});return e!==t}),ve=d(()=>{const e=[];return e.push(a.value.servers.length?`${a.value.servers.length} resolver${a.value.servers.length===1?"":"s"}`:`defaults (${(a.value.defaults||[]).join(", ")||"none"})`),a.value.records.length&&e.push(`${a.value.records.length} override${a.value.records.length===1?"":"s"}`),e.join(" · ")}),fe=async()=>{try{const e=await x.get("/firewall/dns");a.value={servers:e.data.servers||[],search:e.data.search||"",records:e.data.records||[],defaults:e.data.defaults||[]},P.value={servers:[...a.value.servers],search:a.value.search,records:a.value.records.map(t=>({...t}))}}catch(e){console.error("loadDNS failed",e)}},W=()=>{const e=C.value.trim();if(!e)return;if(!(/^[0-9.]+$/.test(e)||e.includes(":"))){p.notify({title:"Invalid IP",message:`"${e}" doesn't look like an IPv4 or IPv6 address.`});return}if(a.value.servers.includes(e)){C.value="";return}a.value.servers=[...a.value.servers,e],C.value=""},me=e=>{a.value.servers=a.value.servers.filter((t,s)=>s!==e)},H=()=>{const e=I.value.trim(),t=A.value.trim();if(!e||!t)return;const s=/^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*$/.test(e),o=/^[0-9.]+$/.test(t)||t.includes(":");if(!s){p.notify({title:"Invalid hostname",message:`"${e}" is not a valid hostname.`});return}if(!o){p.notify({title:"Invalid IP",message:`"${t}" is not a literal IPv4 or IPv6 address.`});return}if((a.value.records||[]).some(D=>D.host===e)){p.notify({title:"Duplicate host",message:`"${e}" already has an override.`});return}a.value.records=[...a.value.records||[],{host:e,ip:t}],I.value="",A.value=""},he=e=>{a.value.records=a.value.records.filter((t,s)=>s!==e)},pe=()=>{a.value.servers=[],a.value.search="",a.value.records=[]},ye=async()=>{U.value=!0;try{const e=await x.put("/firewall/dns",{servers:a.value.servers,search:a.value.search||"",records:a.value.records||[]});a.value={servers:e.data.servers||[],search:e.data.search||"",records:e.data.records||[],defaults:e.data.defaults||a.value.defaults},P.value={servers:[...a.value.servers],search:a.value.search,records:a.value.records.map(t=>({...t}))}}catch(e){p.notify({title:"Save failed",message:e.response?.data?.error?.message||e.message,danger:!0})}finally{U.value=!1}},q=d(()=>b.value.filter(e=>e.kind==="custom")),G={"169.254.0.0/16":{name:"Cloud metadata service",why:"Blocks the special address AWS, Azure, and GCP use to expose VM credentials and instance settings. Leaving this open is a common credential-leak path."},"fd00:ec2::254/128":{name:"Cloud metadata (IPv6)",why:"Same as above, but the IPv6 path GCP uses. Recommended on."},"10.0.0.0/8":{name:"Private network, 10.x",why:"Standard internal-network range. Turn on if your functions should not reach internal services on your LAN."},"172.16.0.0/12":{name:"Private network, 172.16.x",why:"Another internal-network range (often used by Docker default bridge). Turn on for stricter isolation."},"192.168.0.0/16":{name:"Private network, 192.168.x",why:"Common home/office network range. Turn on if functions should not reach your local LAN."},"100.64.0.0/10":{name:"CGNAT / Tailscale",why:"Used by Tailscale and large ISPs. Turn on to keep functions out of your tailnet."}},L=e=>e.kind==="custom"?e.label||e.value:G[e.value]?.name||e.label||e.value,ge=e=>e.kind==="custom"?"":G[e.value]?.why||"",k=f("on"),be=d(()=>[{id:"all",label:"All",count:b.value.length},{id:"on",label:"On",count:b.value.filter(e=>e.enabled).length},{id:"off",label:"Off",count:b.value.filter(e=>!e.enabled).length},{id:"yours",label:"Yours",count:q.value.length}]),K=d(()=>{const e={default:0,suggested:1,custom:2};return[...b.value.filter(s=>k.value==="on"?s.enabled:k.value==="off"?!s.enabled:k.value==="yours"?s.kind==="custom":!0)].sort((s,o)=>s.enabled!==o.enabled?s.enabled?-1:1:e[s.kind]!==e[o.kind]?e[s.kind]-e[o.kind]:L(s).localeCompare(L(o)))}),J=d(()=>b.value.filter(e=>e.enabled).length),Y=d(()=>b.value.filter(e=>!e.enabled).length),ke=d(()=>{if(!b.value.length)return"Nothing in the blocklist yet.";const e=J.value,t=Y.value;return`${e} block${e===1?"":"s"} active · ${t} available to turn on · ${q.value.length} you added`}),we=[{value:"cidr",label:"IP / Range",icon:de},{value:"hostname",label:"Hostname",icon:ne},{value:"wildcard",label:"Pattern",icon:ie}],xe=d(()=>{switch(c.value.rule_type){case"hostname":return"api.internal.corp";case"wildcard":return"*.corp.com";default:return"192.168.1.0/24"}}),_e=d(()=>{switch(c.value.rule_type){case"hostname":return"A specific website or service name. We resolve it to IPs and block those.";case"wildcard":return"Match an entire domain and its subdomains. Use *.example.com for everything under example.com.";default:return"A single IP (e.g. 1.2.3.4) or a CIDR range (e.g. 10.0.0.0/8) to block all addresses inside it."}}),Ce=d(()=>g.value.last_error?"border-red-500/40 bg-red-500/10 text-red-200":g.value.nftables_available?"border-success/30 bg-success/5 text-foreground-muted":"border-amber-500/40 bg-amber-500/10 text-amber-200"),$e=d(()=>g.value.last_error?re:g.value.nftables_available?Be:re),Se=d(()=>g.value.last_error?g.value.last_error:g.value.nftables_available?"Active. Rules apply to every function with outbound network enabled.":"nftables unavailable on this host. Packet-level enforcement is disabled. Sandbox-level isolation still works."),R=async()=>{const e=await x.get("/firewall/rules");b.value=e.data.rules||[],g.value=e.data.status||{ipv4:[],ipv6:[]}},Pe=async e=>{M.value=e.id;try{await x.put(`/firewall/rules/${e.id}`,{enabled:!e.enabled}),await R()}catch(t){p.notify({title:"Toggle failed",message:t.response?.data?.error?.message||t.message,danger:!0})}finally{M.value=null}},Ie=async e=>{if(await p.ask({title:"Delete custom rule?",message:`"${e.value}" will be removed from the blocklist.`,confirmLabel:"Delete",danger:!0}))try{await x.delete(`/firewall/rules/${e.id}`),await R()}catch(s){p.notify({title:"Delete failed",message:s.response?.data?.error?.message||s.message,danger:!0})}},Ae=async()=>{if(c.value.value.trim()){j.value=!0;try{await x.post("/firewall/rules",{rule_type:c.value.rule_type,value:c.value.value.trim(),label:c.value.label.trim()}),_.value=!1,c.value={rule_type:"cidr",value:"",label:""},await R()}catch(e){p.notify({title:"Failed to add rule",message:e.response?.data?.error?.message||e.message,danger:!0})}finally{j.value=!1}}},Re=async()=>{O.value=!0;try{const e=await x.post("/firewall/resolve");e.data.error&&p.notify({title:"Resolve had errors",message:e.data.error,danger:!0}),await R()}catch(e){p.notify({title:"Resolve failed",message:e.response?.data?.error?.message||e.message,danger:!0})}finally{O.value=!1}},Q=async()=>{await Promise.all([R(),fe()])};Te(Q),Ve(Q);const X=le({name:"PanelSection",props:{title:String,subtitle:String},setup(e,{slots:t}){return()=>u("section",{class:"space-y-3"},[u("div",null,[u("h2",{class:"text-sm font-semibold text-white tracking-tight"},e.title),e.subtitle?u("p",{class:"text-xs text-foreground-muted mt-0.5"},e.subtitle):null]),u("div",null,t.default?.())])}}),ee={default:{label:"Recommended",cls:"kind-recommended"},suggested:{label:"Optional",cls:"kind-optional"},custom:{label:"Yours",cls:"kind-yours"}},De=le({name:"RuleCard",props:{rule:{type:Object,required:!0},status:{type:Object,required:!0},busy:{type:Boolean,default:!1},readonlyEdit:{type:Boolean,default:!1}},emits:["toggle","delete"],setup(e,{emit:t}){const s=d(()=>{switch(e.rule.rule_type){case"hostname":return ne;case"wildcard":return ie;default:return de}}),o=d(()=>e.rule.rule_type==="cidr"?[e.rule.value]:e.status.hostname_map?.[e.rule.value]||[]),D=d(()=>L(e.rule)),te=d(()=>ge(e.rule)),se=d(()=>ee[e.rule.kind]||ee.custom);return()=>u("div",{class:["rule-card",e.rule.enabled?"is-on":"is-off"]},[u("div",{class:"rule-card-row"},[u("div",{class:"rule-card-titlewrap"},[u("div",{class:"rule-card-title"},D.value),u("span",{class:["rule-kind-pill",se.value.cls]},se.value.label)]),u("button",{class:["rule-toggle",e.rule.enabled?"on":"off",e.busy?"busy":""],disabled:e.busy,title:e.rule.enabled?"Click to allow":"Click to block",onClick:()=>t("toggle")},[u("span",{class:"rule-toggle-knob"})])]),te.value?u("p",{class:"rule-card-why"},te.value):null,u("div",{class:"rule-card-foot"},[u(s.value,{class:"rule-card-type-icon"}),u("code",{class:"rule-card-value"},e.rule.value),o.value.length&&e.rule.rule_type!=="cidr"?u("span",{class:"rule-card-resolved"},`→ ${o.value.slice(0,2).join(", ")}${o.value.length>2?` +${o.value.length-2}`:""}`):null]),e.readonlyEdit?null:u("button",{class:"rule-card-delete",title:"Remove this block",onClick:()=>t("delete")},[u(je,{class:"w-3.5 h-3.5"})])])}});return(e,t)=>(r(),i("div",He,[l("header",Le,[l("div",Ee,[t[12]||(t[12]=l("div",{class:"max-w-2xl"},[l("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Firewall & DNS "),l("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"}," Decide what your functions are allowed to talk to. Each switch below blocks one destination; turn one on and your functions can no longer reach it; turn it off and they can. DNS settings below control how your functions look hostnames up. ")],-1)),l("div",Ze,[n(w,{variant:"secondary",size:"sm",loading:O.value,onClick:Re},{default:h(()=>[n(m(Me),{class:"w-4 h-4"}),t[10]||(t[10]=y(" Apply now ",-1))]),_:1},8,["loading"]),n(w,{size:"sm",onClick:t[0]||(t[0]=s=>_.value=!0)},{default:h(()=>[n(m(N),{class:"w-4 h-4"}),t[11]||(t[11]=y(" Block something ",-1))]),_:1})])]),l("div",{class:E(["flex items-center gap-3 text-xs px-3 py-2 rounded-md border",Ce.value])},[(r(),V(ae($e.value),{class:"w-4 h-4 shrink-0"})),l("span",Fe,v(Se.value),1),g.value.nftables_available?(r(),i("span",We,v(J.value)+" active · "+v(Y.value)+" off ",1)):Z("",!0)],2)]),n(m(X),{title:"DNS",subtitle:ve.value},{default:h(()=>[l("div",qe,[l("div",Ge,[t[15]||(t[15]=l("div",{class:"dns-row-label"}," Upstream resolvers ",-1)),l("div",Ke,[a.value.servers.length?(r(),i("div",Je,[(r(!0),i($,null,S(a.value.servers,(s,o)=>(r(),i("span",{key:s+o,class:"dns-chip"},[n(m(ue),{class:"w-3 h-3 opacity-60"}),l("span",Ye,v(s),1),l("button",{class:"dns-chip-x",title:"Remove",onClick:D=>me(o)}," × ",8,Qe)]))),128))])):(r(),i("div",Xe,[t[13]||(t[13]=l("span",{class:"text-foreground-muted text-xs"},"Defaults:",-1)),(r(!0),i($,null,S(a.value.defaults,s=>(r(),i("span",{key:s,class:"dns-chip muted"},[n(m(ue),{class:"w-3 h-3 opacity-60"}),l("span",et,v(s),1)]))),128))]))]),l("div",tt,[z(l("input",{"onUpdate:modelValue":t[1]||(t[1]=s=>C.value=s),placeholder:"1.1.1.1",class:"dns-input",onKeydown:F(W,["enter"])},null,544),[[B,C.value]]),n(w,{variant:"secondary",size:"sm",disabled:!C.value.trim(),onClick:W},{default:h(()=>[n(m(N),{class:"w-3.5 h-3.5"}),t[14]||(t[14]=y(" Add resolver ",-1))]),_:1},8,["disabled"]),z(l("input",{"onUpdate:modelValue":t[2]||(t[2]=s=>a.value.search=s),placeholder:"search domain",class:"dns-input narrow"},null,512),[[B,a.value.search]])])]),l("div",st,[l("div",at,[t[16]||(t[16]=y(" Host overrides ",-1)),l("span",lt,v(a.value.records.length)+" record"+v(a.value.records.length===1?"":"s"),1)]),a.value.records.length?(r(),i("div",rt,[(r(!0),i($,null,S(a.value.records,(s,o)=>(r(),i("div",{key:s.host+o,class:"dns-record"},[l("span",ot,v(s.host),1),t[17]||(t[17]=l("span",{class:"text-foreground-muted text-xs"},"→",-1)),l("span",nt,v(s.ip),1),l("button",{class:"dns-chip-x",title:"Remove",onClick:D=>he(o)}," × ",8,it)]))),128))])):(r(),i("div",ut," No overrides. Anything resolves through the upstream resolvers above. ")),l("div",dt,[z(l("input",{"onUpdate:modelValue":t[3]||(t[3]=s=>I.value=s),placeholder:"api.internal",class:"dns-input host",onKeydown:F(H,["enter"])},null,544),[[B,I.value]]),t[19]||(t[19]=l("span",{class:"text-foreground-muted text-xs"},"→",-1)),z(l("input",{"onUpdate:modelValue":t[4]||(t[4]=s=>A.value=s),placeholder:"10.0.5.10",class:"dns-input",onKeydown:F(H,["enter"])},null,544),[[B,A.value]]),n(w,{variant:"secondary",size:"sm",disabled:!(I.value.trim()&&A.value.trim()),onClick:H},{default:h(()=>[n(m(N),{class:"w-3.5 h-3.5"}),t[18]||(t[18]=y(" Add record ",-1))]),_:1},8,["disabled"])])]),l("div",ct,[t[21]||(t[21]=l("span",{class:"dns-hint"}," Records win over upstream DNS. Anything in the override list bypasses resolution entirely. Existing warm workers keep their previous files; toggle the function's network off and on, or wait for idle TTL, to apply. ",-1)),a.value.servers.length||a.value.search||a.value.records.length?(r(),i("button",{key:0,class:"text-[11px] text-foreground-muted hover:text-white px-2 py-1 transition-colors",onClick:pe}," Reset ")):Z("",!0),n(w,{size:"sm",loading:U.value,disabled:!ce.value,onClick:ye},{default:h(()=>[...t[20]||(t[20]=[y(" Save ",-1)])]),_:1},8,["loading","disabled"])])])]),_:1},8,["subtitle"]),n(m(X),{title:"Blocklist",subtitle:ke.value},{default:h(()=>[l("div",vt,[(r(!0),i($,null,S(be.value,s=>(r(),i("button",{key:s.id,class:E(["rule-filter",{active:k.value===s.id}]),onClick:o=>k.value=s.id},[y(v(s.label)+" ",1),l("span",mt,v(s.count),1)],10,ft))),128))]),K.value.length?(r(),i("div",gt,[(r(!0),i($,null,S(K.value,s=>(r(),V(m(De),{key:s.id,rule:s,status:g.value,busy:M.value===s.id,"readonly-edit":s.kind!=="custom",onToggle:o=>Pe(s),onDelete:o=>Ie(s)},null,8,["rule","status","busy","readonly-edit","onToggle","onDelete"]))),128))])):(r(),i("div",ht,[n(m(Ue),{class:"w-5 h-5 mb-2 text-foreground-muted/60"}),l("p",pt,v(k.value==="yours"?"No custom blocks yet":"Nothing matches this filter"),1),l("p",yt,v(k.value==="yours"?"Block a specific IP, CIDR, or hostname to keep your functions out of internal infrastructure they shouldn't reach.":"Try a different filter, or add a custom block above."),1),k.value==="yours"?(r(),V(w,{key:0,class:"mt-4",size:"sm",variant:"secondary",onClick:t[5]||(t[5]=s=>_.value=!0)},{default:h(()=>[n(m(N),{class:"w-3.5 h-3.5"}),t[22]||(t[22]=y(" Block something ",-1))]),_:1})):Z("",!0)]))]),_:1},8,["subtitle"]),n(ze,{modelValue:_.value,"onUpdate:modelValue":t[9]||(t[9]=s=>_.value=s),title:"Block something",icon:m(Oe),size:"md"},{footer:h(()=>[n(w,{variant:"secondary",onClick:t[8]||(t[8]=s=>_.value=!1)},{default:h(()=>[...t[26]||(t[26]=[y(" Cancel ",-1)])]),_:1}),n(w,{loading:j.value,disabled:!c.value.value.trim(),onClick:Ae},{default:h(()=>[n(m(N),{class:"w-4 h-4"}),t[27]||(t[27]=y(" Block it ",-1))]),_:1},8,["loading","disabled"])]),default:h(()=>[l("div",bt,[t[24]||(t[24]=l("p",{class:"text-xs text-foreground-muted leading-snug"}," Pick what you're blocking: a single IP, a network range, or a hostname. Once added, your functions can no longer reach it. ",-1)),l("div",null,[t[23]||(t[23]=l("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-2"}," What is it? ",-1)),l("div",kt,[(r(),i($,null,S(we,s=>l("button",{key:s.value,class:E(["px-2 py-2 rounded border text-xs font-medium transition-colors flex flex-col items-center gap-1",c.value.rule_type===s.value?"bg-white text-black border-white":"bg-surface-hover text-foreground-muted border-border hover:border-foreground-muted"]),onClick:o=>c.value.rule_type=s.value},[(r(),V(ae(s.icon),{class:"w-3.5 h-3.5"})),y(" "+v(s.label),1)],10,wt)),64))]),l("p",xt,v(_e.value),1)]),n(oe,{modelValue:c.value.value,"onUpdate:modelValue":t[6]||(t[6]=s=>c.value.value=s),label:c.value.rule_type==="wildcard"?"Pattern":c.value.rule_type==="hostname"?"Hostname":"IP or network",placeholder:xe.value},null,8,["modelValue","label","placeholder"]),n(oe,{modelValue:c.value.label,"onUpdate:modelValue":t[7]||(t[7]=s=>c.value.label=s),label:"Why? (optional)",placeholder:"e.g. our staging Postgres"},null,8,["modelValue"]),t[25]||(t[25]=l("p",{class:"text-[11px] text-foreground-muted leading-snug"}," Takes effect within seconds. Warm functions are recycled so the new block applies on the very next call. ",-1))])]),_:1},8,["modelValue","icon"])]))}};export{Rt as default}; diff --git a/backend/internal/server/ui_dist/assets/Firewall-BptfP9kb.js b/backend/internal/server/ui_dist/assets/Firewall-BptfP9kb.js new file mode 100644 index 0000000..71e0fea --- /dev/null +++ b/backend/internal/server/ui_dist/assets/Firewall-BptfP9kb.js @@ -0,0 +1 @@ +import{c as N,G as Te,o as Ne,X as Ve,a as i,b as l,d as o,h,_ as w,s as F,n as V,K as ae,t as v,g as E,f as m,r as f,q as d,aF as le,z as u,D as x,j as r,k as g,P as T,F as $,p as S,e as z,R as G,v as B,T as re}from"./index-CmY58qNN.js";import{_ as ne}from"./Input-B5GdDd-T.js";import{_ as ze}from"./Modal-zN5wBHcp.js";import{R as Be}from"./refresh-cw-63KivPtM.js";import{G as oe}from"./globe-D5WsrNJb.js";import{T as Me}from"./trash-2-BJzRpJ7Y.js";import{S as je}from"./shield-check-DChOQFDR.js";const ie=N("asterisk",[["path",{d:"M12 6v12",key:"1vza4d"}],["path",{d:"M17.196 9 6.804 15",key:"1ah31z"}],["path",{d:"m6.804 9 10.392 6",key:"1b6pxd"}]]);const ue=N("earth",[["path",{d:"M21.54 15H17a2 2 0 0 0-2 2v4.54",key:"1djwo0"}],["path",{d:"M7 3.34V5a3 3 0 0 0 3 3a2 2 0 0 1 2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2c0-1.1.9-2 2-2h3.17",key:"1tzkfa"}],["path",{d:"M11 21.95V18a2 2 0 0 0-2-2a2 2 0 0 1-2-2v-1a2 2 0 0 0-2-2H2.05",key:"14pb5j"}],["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}]]);const de=N("hash",[["line",{x1:"4",x2:"20",y1:"9",y2:"9",key:"4lhtct"}],["line",{x1:"4",x2:"20",y1:"15",y2:"15",key:"vyu0kd"}],["line",{x1:"10",x2:"8",y1:"3",y2:"21",key:"1ggp8o"}],["line",{x1:"16",x2:"14",y1:"3",y2:"21",key:"weycgp"}]]);const Oe=N("shield-alert",[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z",key:"oel41y"}],["path",{d:"M12 8v4",key:"1got3b"}],["path",{d:"M12 16h.01",key:"1drbdi"}]]);const Ue=N("shield-off",[["path",{d:"m2 2 20 20",key:"1ooewy"}],["path",{d:"M5 5a1 1 0 0 0-1 1v7c0 5 3.5 7.5 7.67 8.94a1 1 0 0 0 .67.01c2.35-.82 4.48-1.97 5.9-3.71",key:"1jlk70"}],["path",{d:"M9.309 3.652A12.252 12.252 0 0 0 11.24 2.28a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1v7a9.784 9.784 0 0 1-.08 1.264",key:"18rp1v"}]]),He={class:"space-y-8"},Le={class:"space-y-3"},Fe={class:"flex items-start justify-between gap-4 flex-wrap"},Ee={class:"flex items-center gap-2"},Ge={class:"flex-1 min-w-0"},Ke={key:0,class:"text-foreground-muted hidden sm:inline shrink-0"},We={class:"dns-card"},Ze={class:"dns-row"},qe={class:"dns-current"},Je={key:0,class:"dns-chips"},Ye={class:"font-mono"},Xe=["onClick"],Qe={key:1,class:"dns-defaults"},et={class:"font-mono"},tt={class:"dns-form"},st={class:"dns-row"},at={class:"dns-row-label"},lt={class:"dns-row-meta"},rt={key:0,class:"dns-records"},nt={class:"font-mono text-white text-xs flex-1 truncate"},ot={class:"font-mono text-foreground text-xs flex-1 truncate"},it=["onClick"],ut={key:1,class:"text-xs text-foreground-muted italic px-1"},dt={class:"dns-form"},ct={class:"dns-savebar"},vt={class:"rule-filterbar"},ft=["onClick"],mt={class:"rule-filter-count"},ht={key:0,class:"empty-card"},pt={class:"text-sm text-white"},gt={class:"text-xs text-foreground-muted mt-1 max-w-sm"},yt={key:1,class:"rule-grid"},bt={class:"space-y-4"},kt={class:"grid grid-cols-3 gap-2"},wt=["onClick"],xt={class:"text-[10.5px] text-foreground-muted mt-1.5 leading-snug"},Tt={__name:"Firewall",setup(_t){const p=Te(),b=f([]),y=f({ipv4:[],ipv6:[],hostname_map:{},nftables_available:!0,last_error:""}),M=f(null),_=f(!1),j=f(!1),O=f(!1),c=f({rule_type:"cidr",value:"",label:""}),a=f({servers:[],search:"",records:[],defaults:[]}),P=f({servers:[],search:"",records:[]}),C=f(""),I=f(""),A=f(""),U=f(!1),ce=d(()=>{const e=JSON.stringify({s:a.value.servers||[],q:a.value.search||"",r:(a.value.records||[]).map(s=>`${s.host}=${s.ip}`).sort()}),t=JSON.stringify({s:P.value.servers||[],q:P.value.search||"",r:(P.value.records||[]).map(s=>`${s.host}=${s.ip}`).sort()});return e!==t}),ve=d(()=>{const e=[];return e.push(a.value.servers.length?`${a.value.servers.length} resolver${a.value.servers.length===1?"":"s"}`:`defaults (${(a.value.defaults||[]).join(", ")||"none"})`),a.value.records.length&&e.push(`${a.value.records.length} override${a.value.records.length===1?"":"s"}`),e.join(" · ")}),fe=async()=>{try{const e=await x.get("/firewall/dns");a.value={servers:e.data.servers||[],search:e.data.search||"",records:e.data.records||[],defaults:e.data.defaults||[]},P.value={servers:[...a.value.servers],search:a.value.search,records:a.value.records.map(t=>({...t}))}}catch(e){console.error("loadDNS failed",e)}},K=()=>{const e=C.value.trim();if(!e)return;if(!(/^[0-9.]+$/.test(e)||e.includes(":"))){p.notify({title:"Invalid IP",message:`"${e}" doesn't look like an IPv4 or IPv6 address.`});return}if(a.value.servers.includes(e)){C.value="";return}a.value.servers=[...a.value.servers,e],C.value=""},me=e=>{a.value.servers=a.value.servers.filter((t,s)=>s!==e)},H=()=>{const e=I.value.trim(),t=A.value.trim();if(!e||!t)return;const s=/^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*$/.test(e),n=/^[0-9.]+$/.test(t)||t.includes(":");if(!s){p.notify({title:"Invalid hostname",message:`"${e}" is not a valid hostname.`});return}if(!n){p.notify({title:"Invalid IP",message:`"${t}" is not a literal IPv4 or IPv6 address.`});return}if((a.value.records||[]).some(D=>D.host===e)){p.notify({title:"Duplicate host",message:`"${e}" already has an override.`});return}a.value.records=[...a.value.records||[],{host:e,ip:t}],I.value="",A.value=""},he=e=>{a.value.records=a.value.records.filter((t,s)=>s!==e)},pe=()=>{a.value.servers=[],a.value.search="",a.value.records=[]},ge=async()=>{U.value=!0;try{const e=await x.put("/firewall/dns",{servers:a.value.servers,search:a.value.search||"",records:a.value.records||[]});a.value={servers:e.data.servers||[],search:e.data.search||"",records:e.data.records||[],defaults:e.data.defaults||a.value.defaults},P.value={servers:[...a.value.servers],search:a.value.search,records:a.value.records.map(t=>({...t}))}}catch(e){p.notify({title:"Save failed",message:e.response?.data?.error?.message||e.message,danger:!0})}finally{U.value=!1}},W=d(()=>b.value.filter(e=>e.kind==="custom")),Z={"169.254.0.0/16":{name:"Cloud metadata service",why:"Blocks the special address AWS, Azure, and GCP use to expose VM credentials and instance settings. Leaving this open is a common credential-leak path."},"fd00:ec2::254/128":{name:"Cloud metadata (IPv6)",why:"Same as above, but the IPv6 path GCP uses. Recommended on."},"10.0.0.0/8":{name:"Private network, 10.x",why:"Standard internal-network range. Turn on if your functions should not reach internal services on your LAN."},"172.16.0.0/12":{name:"Private network, 172.16.x",why:"Another internal-network range (often used by Docker default bridge). Turn on for stricter isolation."},"192.168.0.0/16":{name:"Private network, 192.168.x",why:"Common home/office network range. Turn on if functions should not reach your local LAN."},"100.64.0.0/10":{name:"CGNAT / Tailscale",why:"Used by Tailscale and large ISPs. Turn on to keep functions out of your tailnet."}},L=e=>e.kind==="custom"?e.label||e.value:Z[e.value]?.name||e.label||e.value,ye=e=>e.kind==="custom"?"":Z[e.value]?.why||"",k=f("on"),be=d(()=>[{id:"all",label:"All",count:b.value.length},{id:"on",label:"On",count:b.value.filter(e=>e.enabled).length},{id:"off",label:"Off",count:b.value.filter(e=>!e.enabled).length},{id:"yours",label:"Yours",count:W.value.length}]),q=d(()=>{const e={default:0,suggested:1,custom:2};return[...b.value.filter(s=>k.value==="on"?s.enabled:k.value==="off"?!s.enabled:k.value==="yours"?s.kind==="custom":!0)].sort((s,n)=>s.enabled!==n.enabled?s.enabled?-1:1:e[s.kind]!==e[n.kind]?e[s.kind]-e[n.kind]:L(s).localeCompare(L(n)))}),J=d(()=>b.value.filter(e=>e.enabled).length),Y=d(()=>b.value.filter(e=>!e.enabled).length),ke=d(()=>{if(!b.value.length)return"Nothing in the blocklist yet.";const e=J.value,t=Y.value;return`${e} block${e===1?"":"s"} active · ${t} available to turn on · ${W.value.length} you added`}),we=[{value:"cidr",label:"IP / Range",icon:de},{value:"hostname",label:"Hostname",icon:oe},{value:"wildcard",label:"Pattern",icon:ie}],xe=d(()=>{switch(c.value.rule_type){case"hostname":return"api.internal.corp";case"wildcard":return"*.corp.com";default:return"192.168.1.0/24"}}),_e=d(()=>{switch(c.value.rule_type){case"hostname":return"A specific website or service name. We resolve it to IPs and block those.";case"wildcard":return"Match an entire domain and its subdomains. Use *.example.com for everything under example.com.";default:return"A single IP (e.g. 1.2.3.4) or a CIDR range (e.g. 10.0.0.0/8) to block all addresses inside it."}}),Ce=d(()=>y.value.last_error?"border-danger-ring bg-danger-tint text-danger-fg":y.value.nftables_available?"border-success-ring bg-success-tint text-foreground-muted":"border-warning-ring bg-warning-tint text-warning-fg"),$e=d(()=>y.value.last_error?re:y.value.nftables_available?je:re),Se=d(()=>y.value.last_error?y.value.last_error:y.value.nftables_available?"Active. Rules apply to every function with outbound network enabled.":"nftables unavailable on this host. Packet-level enforcement is disabled. Sandbox-level isolation still works."),R=async()=>{const e=await x.get("/firewall/rules");b.value=e.data.rules||[],y.value=e.data.status||{ipv4:[],ipv6:[]}},Pe=async e=>{M.value=e.id;try{await x.put(`/firewall/rules/${e.id}`,{enabled:!e.enabled}),await R()}catch(t){p.notify({title:"Toggle failed",message:t.response?.data?.error?.message||t.message,danger:!0})}finally{M.value=null}},Ie=async e=>{if(await p.ask({title:"Delete custom rule?",message:`"${e.value}" will be removed from the blocklist.`,confirmLabel:"Delete",danger:!0}))try{await x.delete(`/firewall/rules/${e.id}`),await R()}catch(s){p.notify({title:"Delete failed",message:s.response?.data?.error?.message||s.message,danger:!0})}},Ae=async()=>{if(c.value.value.trim()){j.value=!0;try{await x.post("/firewall/rules",{rule_type:c.value.rule_type,value:c.value.value.trim(),label:c.value.label.trim()}),_.value=!1,c.value={rule_type:"cidr",value:"",label:""},await R()}catch(e){p.notify({title:"Failed to add rule",message:e.response?.data?.error?.message||e.message,danger:!0})}finally{j.value=!1}}},Re=async()=>{O.value=!0;try{const e=await x.post("/firewall/resolve");e.data.error&&p.notify({title:"Resolve had errors",message:e.data.error,danger:!0}),await R()}catch(e){p.notify({title:"Resolve failed",message:e.response?.data?.error?.message||e.message,danger:!0})}finally{O.value=!1}},X=async()=>{await Promise.all([R(),fe()])};Ne(X),Ve(X);const Q=le({name:"PanelSection",props:{title:String,subtitle:String},setup(e,{slots:t}){return()=>u("section",{class:"space-y-3"},[u("div",null,[u("h2",{class:"text-sm font-semibold text-white tracking-tight"},e.title),e.subtitle?u("p",{class:"text-xs text-foreground-muted mt-0.5"},e.subtitle):null]),u("div",null,t.default?.())])}}),ee={default:{label:"Recommended",cls:"kind-recommended"},suggested:{label:"Optional",cls:"kind-optional"},custom:{label:"Yours",cls:"kind-yours"}},De=le({name:"RuleCard",props:{rule:{type:Object,required:!0},status:{type:Object,required:!0},busy:{type:Boolean,default:!1},readonlyEdit:{type:Boolean,default:!1}},emits:["toggle","delete"],setup(e,{emit:t}){const s=d(()=>{switch(e.rule.rule_type){case"hostname":return oe;case"wildcard":return ie;default:return de}}),n=d(()=>e.rule.rule_type==="cidr"?[e.rule.value]:e.status.hostname_map?.[e.rule.value]||[]),D=d(()=>L(e.rule)),te=d(()=>ye(e.rule)),se=d(()=>ee[e.rule.kind]||ee.custom);return()=>u("div",{class:["rule-card",e.rule.enabled?"is-on":"is-off"]},[u("div",{class:"rule-card-row"},[u("div",{class:"rule-card-titlewrap"},[u("div",{class:"rule-card-title"},D.value),u("span",{class:["rule-kind-pill",se.value.cls]},se.value.label)]),u("button",{class:["rule-toggle",e.rule.enabled?"on":"off",e.busy?"busy":"","focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background"],disabled:e.busy,title:e.rule.enabled?"Click to allow":"Click to block",onClick:()=>t("toggle")},[u("span",{class:"rule-toggle-knob"})])]),te.value?u("p",{class:"rule-card-why"},te.value):null,u("div",{class:"rule-card-foot"},[u(s.value,{class:"rule-card-type-icon"}),u("code",{class:"rule-card-value"},e.rule.value),n.value.length&&e.rule.rule_type!=="cidr"?u("span",{class:"rule-card-resolved"},`→ ${n.value.slice(0,2).join(", ")}${n.value.length>2?` +${n.value.length-2}`:""}`):null]),e.readonlyEdit?null:u("button",{class:"rule-card-delete",title:"Remove this block",onClick:()=>t("delete")},[u(Me,{class:"w-3.5 h-3.5"})])])}});return(e,t)=>(r(),i("div",He,[l("header",Le,[l("div",Fe,[t[12]||(t[12]=l("div",{class:"max-w-2xl"},[l("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Firewall & DNS "),l("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"}," Decide what your functions are allowed to talk to. Each switch below blocks one destination; turn one on and your functions can no longer reach it; turn it off and they can. DNS settings below control how your functions look hostnames up. ")],-1)),l("div",Ee,[o(w,{variant:"secondary",size:"sm",loading:O.value,onClick:Re},{default:h(()=>[o(m(Be),{class:"w-4 h-4"}),t[10]||(t[10]=g(" Apply now ",-1))]),_:1},8,["loading"]),o(w,{size:"sm",onClick:t[0]||(t[0]=s=>_.value=!0)},{default:h(()=>[o(m(T),{class:"w-4 h-4"}),t[11]||(t[11]=g(" Block something ",-1))]),_:1})])]),l("div",{class:F(["flex items-center gap-3 text-xs px-3 py-2 rounded-md border",Ce.value])},[(r(),V(ae($e.value),{class:"w-4 h-4 shrink-0"})),l("span",Ge,v(Se.value),1),y.value.nftables_available?(r(),i("span",Ke,v(J.value)+" active · "+v(Y.value)+" off ",1)):E("",!0)],2)]),o(m(Q),{title:"DNS",subtitle:ve.value},{default:h(()=>[l("div",We,[l("div",Ze,[t[15]||(t[15]=l("div",{class:"dns-row-label"}," Upstream resolvers ",-1)),l("div",qe,[a.value.servers.length?(r(),i("div",Je,[(r(!0),i($,null,S(a.value.servers,(s,n)=>(r(),i("span",{key:s+n,class:"dns-chip"},[o(m(ue),{class:"w-3 h-3 opacity-60"}),l("span",Ye,v(s),1),l("button",{class:"dns-chip-x",title:"Remove",onClick:D=>me(n)}," × ",8,Xe)]))),128))])):(r(),i("div",Qe,[t[13]||(t[13]=l("span",{class:"text-foreground-muted text-xs"},"Defaults:",-1)),(r(!0),i($,null,S(a.value.defaults,s=>(r(),i("span",{key:s,class:"dns-chip muted"},[o(m(ue),{class:"w-3 h-3 opacity-60"}),l("span",et,v(s),1)]))),128))]))]),l("div",tt,[z(l("input",{"onUpdate:modelValue":t[1]||(t[1]=s=>C.value=s),placeholder:"1.1.1.1",class:"dns-input",onKeydown:G(K,["enter"])},null,544),[[B,C.value]]),o(w,{variant:"secondary",size:"sm",disabled:!C.value.trim(),onClick:K},{default:h(()=>[o(m(T),{class:"w-3.5 h-3.5"}),t[14]||(t[14]=g(" Add resolver ",-1))]),_:1},8,["disabled"]),z(l("input",{"onUpdate:modelValue":t[2]||(t[2]=s=>a.value.search=s),placeholder:"search domain",class:"dns-input narrow"},null,512),[[B,a.value.search]])])]),l("div",st,[l("div",at,[t[16]||(t[16]=g(" Host overrides ",-1)),l("span",lt,v(a.value.records.length)+" record"+v(a.value.records.length===1?"":"s"),1)]),a.value.records.length?(r(),i("div",rt,[(r(!0),i($,null,S(a.value.records,(s,n)=>(r(),i("div",{key:s.host+n,class:"dns-record"},[l("span",nt,v(s.host),1),t[17]||(t[17]=l("span",{class:"text-foreground-muted text-xs"},"→",-1)),l("span",ot,v(s.ip),1),l("button",{class:"dns-chip-x",title:"Remove",onClick:D=>he(n)}," × ",8,it)]))),128))])):(r(),i("div",ut," No overrides. Anything resolves through the upstream resolvers above. ")),l("div",dt,[z(l("input",{"onUpdate:modelValue":t[3]||(t[3]=s=>I.value=s),placeholder:"api.internal",class:"dns-input host",onKeydown:G(H,["enter"])},null,544),[[B,I.value]]),t[19]||(t[19]=l("span",{class:"text-foreground-muted text-xs"},"→",-1)),z(l("input",{"onUpdate:modelValue":t[4]||(t[4]=s=>A.value=s),placeholder:"10.0.5.10",class:"dns-input",onKeydown:G(H,["enter"])},null,544),[[B,A.value]]),o(w,{variant:"secondary",size:"sm",disabled:!(I.value.trim()&&A.value.trim()),onClick:H},{default:h(()=>[o(m(T),{class:"w-3.5 h-3.5"}),t[18]||(t[18]=g(" Add record ",-1))]),_:1},8,["disabled"])])]),l("div",ct,[t[21]||(t[21]=l("span",{class:"dns-hint"}," Records win over upstream DNS. Anything in the override list bypasses resolution entirely. Existing warm workers keep their previous files; toggle the function's network off and on, or wait for idle TTL, to apply. ",-1)),a.value.servers.length||a.value.search||a.value.records.length?(r(),i("button",{key:0,class:"text-[11px] text-foreground-muted hover:text-white px-2 py-1 transition-colors",onClick:pe}," Reset ")):E("",!0),o(w,{size:"sm",loading:U.value,disabled:!ce.value,onClick:ge},{default:h(()=>[...t[20]||(t[20]=[g(" Save ",-1)])]),_:1},8,["loading","disabled"])])])]),_:1},8,["subtitle"]),o(m(Q),{title:"Blocklist",subtitle:ke.value},{default:h(()=>[l("div",vt,[(r(!0),i($,null,S(be.value,s=>(r(),i("button",{key:s.id,class:F(["rule-filter",{active:k.value===s.id}]),onClick:n=>k.value=s.id},[g(v(s.label)+" ",1),l("span",mt,v(s.count),1)],10,ft))),128))]),q.value.length?(r(),i("div",yt,[(r(!0),i($,null,S(q.value,s=>(r(),V(m(De),{key:s.id,rule:s,status:y.value,busy:M.value===s.id,"readonly-edit":s.kind!=="custom",onToggle:n=>Pe(s),onDelete:n=>Ie(s)},null,8,["rule","status","busy","readonly-edit","onToggle","onDelete"]))),128))])):(r(),i("div",ht,[o(m(Ue),{class:"w-5 h-5 mb-2 text-foreground-muted/60"}),l("p",pt,v(k.value==="yours"?"No custom blocks yet":"Nothing matches this filter"),1),l("p",gt,v(k.value==="yours"?"Block a specific IP, CIDR, or hostname to keep your functions out of internal infrastructure they shouldn't reach.":"Try a different filter, or add a custom block above."),1),k.value==="yours"?(r(),V(w,{key:0,class:"mt-4",size:"sm",variant:"secondary",onClick:t[5]||(t[5]=s=>_.value=!0)},{default:h(()=>[o(m(T),{class:"w-3.5 h-3.5"}),t[22]||(t[22]=g(" Block something ",-1))]),_:1})):E("",!0)]))]),_:1},8,["subtitle"]),o(ze,{modelValue:_.value,"onUpdate:modelValue":t[9]||(t[9]=s=>_.value=s),title:"Block something",icon:m(Oe),size:"md"},{footer:h(()=>[o(w,{variant:"secondary",onClick:t[8]||(t[8]=s=>_.value=!1)},{default:h(()=>[...t[26]||(t[26]=[g(" Cancel ",-1)])]),_:1}),o(w,{loading:j.value,disabled:!c.value.value.trim(),onClick:Ae},{default:h(()=>[o(m(T),{class:"w-4 h-4"}),t[27]||(t[27]=g(" Block it ",-1))]),_:1},8,["loading","disabled"])]),default:h(()=>[l("div",bt,[t[24]||(t[24]=l("p",{class:"text-xs text-foreground-muted leading-snug"}," Pick what you're blocking: a single IP, a network range, or a hostname. Once added, your functions can no longer reach it. ",-1)),l("div",null,[t[23]||(t[23]=l("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-2"}," What is it? ",-1)),l("div",kt,[(r(),i($,null,S(we,s=>l("button",{key:s.value,class:F(["px-2 py-2 rounded border text-xs font-medium transition-colors flex flex-col items-center gap-1",c.value.rule_type===s.value?"bg-white text-black border-white":"bg-surface-hover text-foreground-muted border-border hover:border-foreground-muted"]),onClick:n=>c.value.rule_type=s.value},[(r(),V(ae(s.icon),{class:"w-3.5 h-3.5"})),g(" "+v(s.label),1)],10,wt)),64))]),l("p",xt,v(_e.value),1)]),o(ne,{modelValue:c.value.value,"onUpdate:modelValue":t[6]||(t[6]=s=>c.value.value=s),label:c.value.rule_type==="wildcard"?"Pattern":c.value.rule_type==="hostname"?"Hostname":"IP or network",placeholder:xe.value},null,8,["modelValue","label","placeholder"]),o(ne,{modelValue:c.value.label,"onUpdate:modelValue":t[7]||(t[7]=s=>c.value.label=s),label:"Why? (optional)",placeholder:"e.g. our staging Postgres"},null,8,["modelValue"]),t[25]||(t[25]=l("p",{class:"text-[11px] text-foreground-muted leading-snug"}," Takes effect within seconds. Warm functions are recycled so the new block applies on the very next call. ",-1))])]),_:1},8,["modelValue","icon"])]))}};export{Tt as default}; diff --git a/backend/internal/server/ui_dist/assets/FunctionDiff-BHKRx2Yw.js b/backend/internal/server/ui_dist/assets/FunctionDiff-BHKRx2Yw.js new file mode 100644 index 0000000..404594f --- /dev/null +++ b/backend/internal/server/ui_dist/assets/FunctionDiff-BHKRx2Yw.js @@ -0,0 +1,10 @@ +import{c as Bt,G as sn,o as an,a0 as Ke,aj as et,I as Le,y as ln,a as C,b as w,k as Q,d as N,h as xe,f as I,t as L,_ as tt,F as de,p as we,g as W,n as dn,s as Pe,r as P,q as $,am as cn,a5 as un,L as nt,i as fn,ab as hn,j as k,ak as mn,ah as gn}from"./index-CmY58qNN.js";import{d as rt}from"./rollbackDiff-Cvt2Ss82.js";import{c as pn}from"./clipboard-CmSw2rR-.js";import{P as _e,E as T,a as Be,C as Mt,S as ne,b as _t,T as Dt,c as vn,d as he,D as E,V as bn,F as St,g as Rt,R as re,W as De,G as $e,l as xn,h as wn,e as Lt,f as kn,s as Cn,t as Z,L as yn,i as On,j as An,k as Bn,m as it,n as Mn,o as _n,p as Dn,q as Sn,r as Rn,u as Ln}from"./index-BOWx3BJu.js";import{C as Pn}from"./copy-BzujsGZw.js";import{Z as Tn}from"./zap-TwPYrLej.js";import{S as En}from"./settings-2-CJPaZu6R.js";import{C as ot}from"./chevron-down-BMYhN6hn.js";import{F as Qn,P as Nn}from"./package-DGq9n3E3.js";import{R as Vn}from"./rotate-ccw-DQrtkmfg.js";const Gn=Bt("arrow-left-right",[["path",{d:"M8 3 4 7l4 4",key:"9rb6wj"}],["path",{d:"M4 7h16",key:"6tx8e3"}],["path",{d:"m16 21 4-4-4-4",key:"siv7j2"}],["path",{d:"M20 17H4",key:"h6l3hr"}]]);const $n=Bt("list",[["path",{d:"M3 5h.01",key:"18ugdj"}],["path",{d:"M3 12h.01",key:"nlz23k"}],["path",{d:"M3 19h.01",key:"noohij"}],["path",{d:"M8 5h13",key:"1pao27"}],["path",{d:"M8 12h13",key:"1za7za"}],["path",{d:"M8 19h13",key:"m83p4d"}]]);class M{constructor(e,r,s,i){this.fromA=e,this.toA=r,this.fromB=s,this.toB=i}offset(e,r=e){return new M(this.fromA+e,this.toA+e,this.fromB+r,this.toB+r)}}function J(n,e,r,s,i,t){if(n==s)return[];let o=Fe(n,e,r,s,i,t),a=Ie(n,e+o,r,s,i+o,t);e+=o,r-=a,i+=o,t-=a;let l=r-e,u=t-i;if(!l||!u)return[new M(e,r,i,t)];if(l>u){let c=n.slice(e,r).indexOf(s.slice(i,t));if(c>-1)return[new M(e,e+c,i,i),new M(e+c+u,r,t,t)]}else if(u>l){let c=s.slice(i,t).indexOf(n.slice(e,r));if(c>-1)return[new M(e,e,i,i+c),new M(r,r,i+c+l,t)]}if(l==1||u==1)return[new M(e,r,i,t)];let d=Et(n,e,r,s,i,t);if(d){let[c,m,g]=d;return J(n,e,c,s,i,m).concat(J(n,c+g,r,s,m+g,t))}return jn(n,e,r,s,i,t)}let ce=1e9,ue=0,je=!1;function jn(n,e,r,s,i,t){let o=r-e,a=t-i;if(ce<1e9&&Math.min(o,a)>ce*16||ue>0&&Date.now()>ue)return Math.min(o,a)>ce*64?[new M(e,r,i,t)]:st(n,e,r,s,i,t);let l=Math.ceil((o+a)/2);Te.reset(l),Ee.reset(l);let u=(g,p)=>n.charCodeAt(e+g)==s.charCodeAt(i+p),d=(g,p)=>n.charCodeAt(r-g-1)==s.charCodeAt(t-p-1),c=(o-a)%2!=0?Ee:null,m=c?null:Te;for(let g=0;gce||ue>0&&!(g&63)&&Date.now()>ue)return st(n,e,r,s,i,t);let p=Te.advance(g,o,a,l,c,!1,u)||Ee.advance(g,o,a,l,m,!0,d);if(p)return Fn(n,e,r,e+p[0],s,i,t,i+p[1])}return[new M(e,r,i,t)]}class Pt{constructor(){this.vec=[]}reset(e){this.len=e<<1;for(let r=0;rr)this.end+=2;else if(c>s)this.start+=2;else if(t){let m=i+(r-s)-l;if(m>=0&&m=r-d)return[g,i+g-m]}else{let g=r-t.vec[m];if(d>=g)return[d,c]}}}return null}}const Te=new Pt,Ee=new Pt;function Fn(n,e,r,s,i,t,o,a){let l=!1;return!ie(n,s)&&++s==r&&(l=!0),!ie(i,a)&&++a==o&&(l=!0),l?[new M(e,r,t,o)]:J(n,e,s,i,t,a).concat(J(n,s,r,i,a,o))}function Tt(n,e){let r=1,s=Math.min(n,e);for(;rr||d>t||n.slice(a,u)!=s.slice(l,d)){if(o==1)return a-e-(ie(n,a)?0:1);o=o>>1}else{if(u==r||d==t)return u-e;a=u,l=d}}}function Ie(n,e,r,s,i,t){if(e==r||i==t||n.charCodeAt(r-1)!=s.charCodeAt(t-1))return 0;let o=Tt(r-e,t-i);for(let a=r,l=t;;){let u=a-o,d=l-o;if(u>1}else{if(u==e||d==i)return r-u;a=u,l=d}}}function Qe(n,e,r,s,i,t,o,a){let l=s.slice(i,t),u=null;for(;;){if(u||o=r)break;let m=n.slice(d,c),g=-1;for(;(g=l.indexOf(m,g+1))!=-1;){let p=Fe(n,c,r,s,i+g+m.length,t),x=Ie(n,e,d,s,i,i+g),v=m.length+p+x;(!u||u[2]>1}}function Et(n,e,r,s,i,t){let o=r-e,a=t-i;if(oi.fromA-e&&s.toB>i.fromB-e&&(n[r-1]=new M(s.fromA,i.toA,s.fromB,i.toB),n.splice(r--,1))}}function In(n,e,r){for(;;){Qt(r,1);let s=!1;for(let i=0;i3||a>3){let l=i==n.length-1?e.length:n[i+1].fromA,u=t.fromA-s,d=l-t.toA,c=lt(e,t.fromA,u),m=at(e,t.toA,d),g=t.fromA-c,p=m-t.toA;if((!o||!a)&&g&&p){let x=Math.max(o,a),[v,y,D]=o?[e,t.fromA,t.toA]:[r,t.fromB,t.toB];x>g&&e.slice(c,t.fromA)==v.slice(D-g,D)?(t=n[i]=new M(c,c+o,t.fromB-g,t.toB-g),c=t.fromA,m=at(e,t.toA,l-t.toA)):x>p&&e.slice(t.toA,m)==v.slice(y,y+p)&&(t=n[i]=new M(m-o,m,t.fromB+p,t.toB+p),m=t.toA,c=lt(e,t.fromA,t.fromA-s)),g=t.fromA-c,p=m-t.toA}if(g||p)t=n[i]=new M(t.fromA-g,t.toA+p,t.fromB-g,t.toB+p);else if(o){if(!a){let x=ct(e,t.fromA,t.toA),v,y=x<0?-1:dt(e,t.toA,t.fromA);x>-1&&(v=x-t.fromA)<=d&&e.slice(t.fromA,x)==e.slice(t.toA,t.toA+v)?t=n[i]=t.offset(v):y>-1&&(v=t.toA-y)<=u&&e.slice(t.fromA-v,t.fromA)==e.slice(y,t.toA)&&(t=n[i]=t.offset(-v))}}else{let x=ct(r,t.fromB,t.toB),v,y=x<0?-1:dt(r,t.toB,t.fromB);x>-1&&(v=x-t.fromB)<=d&&r.slice(t.fromB,x)==r.slice(t.toB,t.toB+v)?t=n[i]=t.offset(v):y>-1&&(v=t.toB-y)<=u&&r.slice(t.fromB-v,t.fromB)==r.slice(y,t.toB)&&(t=n[i]=t.offset(-v))}}s=t.toA}return Qt(n,3),n}let X;try{X=new RegExp("[\\p{Alphabetic}\\p{Number}]","u")}catch{}function Nt(n){return n>48&&n<58||n>64&&n<91||n>96&&n<123}function Vt(n,e){if(e==n.length)return 0;let r=n.charCodeAt(e);return r<192?Nt(r)?1:0:X?!jt(r)||e==n.length-1?X.test(String.fromCharCode(r))?1:0:X.test(n.slice(e,e+2))?2:0:0}function Gt(n,e){if(!e)return 0;let r=n.charCodeAt(e-1);return r<192?Nt(r)?1:0:X?!Ft(r)||e==1?X.test(String.fromCharCode(r))?1:0:X.test(n.slice(e-2,e))?2:0:0}const $t=8;function at(n,e,r){if(e==n.length||!Gt(n,e))return e;for(let s=e,i=e+r,t=0;t<$t;t++){let o=Vt(n,s);if(!o||s+o>i)return s;s+=o}return e}function lt(n,e,r){if(!e||!Vt(n,e))return e;for(let s=e,i=e-r,t=0;t<$t;t++){let o=Gt(n,s);if(!o||s-on>=55296&&n<=56319,Ft=n=>n>=56320&&n<=57343;function ie(n,e){return!e||e==n.length||!jt(n.charCodeAt(e-1))||!Ft(n.charCodeAt(e))}function Hn(n,e,r){var s;let i=r?.override;return i?i(n,e):(ce=((s=r?.scanLimit)!==null&&s!==void 0?s:1e9)>>1,ue=r?.timeout?Date.now()+r.timeout:0,je=!1,In(n,e,J(n,0,n.length,e,0,e.length)))}function It(){return!je}function Wt(n,e,r){return Wn(Hn(n,e,r),n,e)}const V=St.define({combine:n=>n[0]}),Ne=ne.define(),Ht=St.define(),j=he.define({create(n){return null},update(n,e){for(let r of e.effects)r.is(Ne)&&(n=r.value);for(let r of e.state.facet(Ht))n=r(n,e);return n}});class q{constructor(e,r,s,i,t,o=!0){this.changes=e,this.fromA=r,this.toA=s,this.fromB=i,this.toB=t,this.precise=o}offset(e,r){return e||r?new q(this.changes,this.fromA+e,this.toA+e,this.fromB+r,this.toB+r,this.precise):this}get endA(){return Math.max(this.fromA,this.toA-1)}get endB(){return Math.max(this.fromB,this.toB-1)}static build(e,r,s){let i=Wt(e.toString(),r.toString(),s);return qt(i,e,r,0,0,It())}static updateA(e,r,s,i,t){return gt(mt(e,i,!0,s.length),e,r,s,t)}static updateB(e,r,s,i,t){return gt(mt(e,i,!1,r.length),e,r,s,t)}}function ut(n,e,r,s){let i=r.lineAt(n),t=s.lineAt(e);return i.to==n&&t.to==e&&nc+1&&v>m+1)break;g.push(p.offset(-u+s,-d+i)),[c,m]=ft(p.toA+s,p.toB+i,e,r),a++}o.push(new q(g,u,Math.max(u,c),d,Math.max(d,m),t))}return o}const ke=1e3;function ht(n,e,r,s){let i=0,t=n.length;for(;;){if(i==t){let d=0,c=0;i&&({toA:d,toB:c}=n[i-1]);let m=e-(r?d:c);return[d+m,c+m]}let o=i+t>>1,a=n[o],[l,u]=r?[a.fromA,a.toA]:[a.fromB,a.toB];if(l>e)t=o;else if(u<=e)i=o+1;else return s?[a.fromA,a.fromB]:[a.toA,a.toB]}}function mt(n,e,r,s){let i=[];return e.iterChangedRanges((t,o,a,l)=>{let u=0,d=r?e.length:s,c=0,m=r?s:e.length;t>ke&&([u,c]=ht(n,t-ke,r,!0)),o=u?i[i.length-1]={fromA:p.fromA,fromB:p.fromB,toA:d,toB:m,diffA:p.diffA+x,diffB:p.diffB+v}:i.push({fromA:u,toA:d,fromB:c,toB:m,diffA:x,diffB:v})}),i}function gt(n,e,r,s,i){if(!n.length)return e;let t=[];for(let o=0,a=0,l=0,u=0;;o++){let d=o==n.length?null:n[o],c=d?d.fromA+a:r.length,m=d?d.fromB+l:s.length;for(;uc||v.toB+l>m))break;t.push(v.offset(a,l)),u++}if(!d)break;let g=d.toA+a+d.diffA,p=d.toB+l+d.diffB,x=Wt(r.sliceString(c,g),s.sliceString(m,p),i);for(let v of qt(x,r,s,c,m,It()))t.push(v);for(a+=d.diffA,l+=d.diffB;ug&&v.fromB+l>p)break;u++}}return t}const Ut={scanLimit:500},Se=bn.fromClass(class{constructor(n){({deco:this.deco,gutter:this.gutter}=bt(n))}update(n){(n.docChanged||n.viewportChanged||qn(n.startState,n.state)||Un(n.startState,n.state))&&({deco:this.deco,gutter:this.gutter}=bt(n.view))}},{decorations:n=>n.deco}),Ce=_e.low(Rt({class:"cm-changeGutter",markers:n=>{var e;return((e=n.plugin(Se))===null||e===void 0?void 0:e.gutter)||Lt.empty}}));function qn(n,e){return n.field(j,!1)!=e.field(j,!1)}function Un(n,e){return n.facet(V)!=e.facet(V)}const pt=E.line({class:"cm-changedLine"}),Zt=E.mark({class:"cm-changedText"}),Zn=E.mark({tagName:"ins",class:"cm-insertedLine"}),Yn=E.mark({tagName:"del",class:"cm-deletedLine"}),vt=new class extends $e{constructor(){super(...arguments),this.elementClass="cm-changedLineGutter"}};function zn(n,e,r,s,i,t){let o=r?n.fromA:n.fromB,a=r?n.toA:n.toB,l=0;if(o!=a){i.add(o,o,pt),i.add(o,a,r?Yn:Zn),t&&t.add(o,o,vt);for(let u=e.iterRange(o,a-1),d=o;!u.next().done;){if(u.lineBreak){d++,i.add(d,d,pt),t&&t.add(d,d,vt);continue}let c=d+u.value.length;if(s)for(;l=d)break;(o?c.toA:c.toB)>u&&(!t||!t(n.state,c,a,l))&&zn(c,n.state.doc,o,s,a,l)}return{deco:a.finish(),gutter:l&&l.finish()}}class ye extends De{constructor(e){super(),this.height=e}eq(e){return this.height==e.height}toDOM(){let e=document.createElement("div");return e.className="cm-mergeSpacer",e.style.height=this.height+"px",e}updateDOM(e){return e.style.height=this.height+"px",!0}get estimatedHeight(){return this.height}ignoreEvent(){return!1}}const Me=ne.define({map:(n,e)=>n.map(e)}),fe=he.define({create:()=>E.none,update:(n,e)=>{for(let r of e.effects)if(r.is(Me))return r.value;return n.map(e.changes)},provide:n=>T.decorations.from(n)}),Oe=.01;function xt(n,e){if(n.size!=e.size)return!1;let r=n.iter(),s=e.iter();for(;r.value;){if(r.from!=s.from||Math.abs(r.value.spec.widget.height-s.value.spec.widget.height)>1)return!1;r.next(),s.next()}return!0}function Xn(n,e,r){let s=new re,i=new re,t=n.state.field(fe).iter(),o=e.state.field(fe).iter(),a=0,l=0,u=0,d=0,c=n.viewport,m=e.viewport;for(let v=0;;v++){let y=vOe&&(d+=B,i.add(l,l,E.widget({widget:new ye(B),block:!0,side:-1})))}if(D>a+1e3&&ac.from&&lm.from){let _=Math.min(c.from-a,m.from-l);a+=_,l+=_,v--}else if(y)a=y.toA,l=y.toB;else break;for(;t.value&&t.fromOe&&i.add(e.state.doc.length,e.state.doc.length,E.widget({widget:new ye(g),block:!0,side:1}));let p=s.finish(),x=i.finish();xt(p,n.state.field(fe))||n.dispatch({effects:Me.of(p)}),xt(x,e.state.field(fe))||e.dispatch({effects:Me.of(x)})}const Ve=ne.define({map:(n,e)=>e.mapPos(n)});class Jn extends De{constructor(e){super(),this.lines=e}eq(e){return this.lines==e.lines}toDOM(e){let r=document.createElement("div");return r.className="cm-collapsedLines",r.textContent=e.state.phrase("$ unchanged lines",this.lines),r.addEventListener("click",s=>{let i=e.posAtDOM(s.target);e.dispatch({effects:Ve.of(i)});let{side:t,sibling:o}=e.state.facet(V);o&&o().dispatch({effects:Ve.of(Kn(i,e.state.field(j),t=="a"))})}),r}ignoreEvent(e){return e instanceof MouseEvent}get estimatedHeight(){return 27}get type(){return"collapsed-unchanged-code"}}function Kn(n,e,r){let s=0,i=0;for(let t=0;;t++){let o=t=n)return i+(n-s);[s,i]=r?[o.toA,o.toB]:[o.toB,o.toA]}}const er=he.define({create(n){return E.none},update(n,e){n=n.map(e.changes);for(let r of e.effects)r.is(Ve)&&(n=n.update({filter:s=>s!=r.value}));return n},provide:n=>T.decorations.from(n)});function Ge({margin:n=3,minSize:e=4}){return er.init(r=>tr(r,n,e))}function tr(n,e,r){let s=new re,i=n.facet(V).side=="a",t=n.field(j),o=1;for(let a=0;;a++){let l=a=r&&s.add(n.doc.line(u).from,n.doc.line(d).to,E.replace({widget:new Jn(c),block:!0})),!l)break;o=n.doc.lineAt(Math.min(n.doc.length,i?l.toA:l.toB)).number}return s.finish()}const nr=T.styleModule.of(new vn({".cm-mergeView":{overflowY:"auto"},".cm-mergeViewEditors":{display:"flex",alignItems:"stretch"},".cm-mergeViewEditor":{flexGrow:1,flexBasis:0,overflow:"hidden"},".cm-merge-revert":{width:"1.6em",flexGrow:0,flexShrink:0,position:"relative"},".cm-merge-revert button":{position:"absolute",display:"block",width:"100%",boxSizing:"border-box",textAlign:"center",background:"none",border:"none",font:"inherit",cursor:"pointer"}})),Yt=T.baseTheme({".cm-mergeView & .cm-scroller, .cm-mergeView &":{height:"auto !important",overflowY:"visible !important"},"&.cm-merge-a .cm-changedLine, .cm-deletedChunk":{backgroundColor:"rgba(160, 128, 100, .08)"},"&.cm-merge-b .cm-changedLine, .cm-inlineChangedLine":{backgroundColor:"rgba(100, 160, 128, .08)"},"&light.cm-merge-a .cm-changedText, &light .cm-deletedChunk .cm-deletedText":{background:"linear-gradient(#ee443366, #ee443366) bottom/100% 2px no-repeat"},"&dark.cm-merge-a .cm-changedText, &dark .cm-deletedChunk .cm-deletedText":{background:"linear-gradient(#ffaa9966, #ffaa9966) bottom/100% 2px no-repeat"},"&light.cm-merge-b .cm-changedText":{background:"linear-gradient(#22bb22aa, #22bb22aa) bottom/100% 2px no-repeat"},"&dark.cm-merge-b .cm-changedText":{background:"linear-gradient(#88ff88aa, #88ff88aa) bottom/100% 2px no-repeat"},"&.cm-merge-b .cm-deletedText":{background:"#ff000033"},".cm-insertedLine, .cm-deletedLine, .cm-deletedLine del":{textDecoration:"none"},".cm-deletedChunk":{paddingLeft:"6px","& .cm-chunkButtons":{position:"absolute",insetInlineEnd:"5px"},"& button":{border:"none",cursor:"pointer",color:"white",margin:"0 2px",borderRadius:"3px","&[name=accept]":{background:"#2a2"},"&[name=reject]":{background:"#d43"}}},".cm-collapsedLines":{padding:"5px 5px 5px 10px",cursor:"pointer","&:before":{content:'"⦚"',marginInlineEnd:"7px"},"&:after":{content:'"⦚"',marginInlineStart:"7px"}},"&light .cm-collapsedLines":{color:"#444",background:"linear-gradient(to bottom, transparent 0, #f3f3f3 30%, #f3f3f3 70%, transparent 100%)"},"&dark .cm-collapsedLines":{color:"#ddd",background:"linear-gradient(to bottom, transparent 0, #222 30%, #222 70%, transparent 100%)"},".cm-changeGutter":{width:"3px",paddingLeft:"1px"},"&light.cm-merge-a .cm-changedLineGutter, &light .cm-deletedLineGutter":{background:"#e43"},"&dark.cm-merge-a .cm-changedLineGutter, &dark .cm-deletedLineGutter":{background:"#fa9"},"&light.cm-merge-b .cm-changedLineGutter":{background:"#2b2"},"&dark.cm-merge-b .cm-changedLineGutter":{background:"#8f8"},".cm-inlineChangedLineGutter":{background:"#75d"}}),wt=new Mt,Ae=new Mt;class rr{constructor(e){this.revertDOM=null,this.revertToA=!1,this.revertToLeft=!1,this.measuring=-1,this.diffConf=e.diffConfig||Ut;let r=[_e.low(Se),Yt,nr,fe,T.updateListener.of(c=>{this.measuring<0&&(c.heightChanged||c.viewportChanged)&&!c.transactions.some(m=>m.effects.some(g=>g.is(Me)))&&this.measure()})],s=[V.of({side:"a",sibling:()=>this.b,highlightChanges:e.highlightChanges!==!1,markGutter:e.gutter!==!1})];e.gutter!==!1&&s.push(Ce);let i=Be.create({doc:e.a.doc,selection:e.a.selection,extensions:[e.a.extensions||[],T.editorAttributes.of({class:"cm-merge-a"}),Ae.of(s),r]}),t=[V.of({side:"b",sibling:()=>this.a,highlightChanges:e.highlightChanges!==!1,markGutter:e.gutter!==!1})];e.gutter!==!1&&t.push(Ce);let o=Be.create({doc:e.b.doc,selection:e.b.selection,extensions:[e.b.extensions||[],T.editorAttributes.of({class:"cm-merge-b"}),Ae.of(t),r]});this.chunks=q.build(i.doc,o.doc,this.diffConf);let a=[j.init(()=>this.chunks),wt.of(e.collapseUnchanged?Ge(e.collapseUnchanged):[])];i=i.update({effects:ne.appendConfig.of(a)}).state,o=o.update({effects:ne.appendConfig.of(a)}).state,this.dom=document.createElement("div"),this.dom.className="cm-mergeView",this.editorDOM=this.dom.appendChild(document.createElement("div")),this.editorDOM.className="cm-mergeViewEditors";let l=e.orientation||"a-b",u=document.createElement("div");u.className="cm-mergeViewEditor";let d=document.createElement("div");d.className="cm-mergeViewEditor",this.editorDOM.appendChild(l=="a-b"?u:d),this.editorDOM.appendChild(l=="a-b"?d:u),this.a=new T({state:i,parent:u,root:e.root,dispatchTransactions:c=>this.dispatch(c,this.a)}),this.b=new T({state:o,parent:d,root:e.root,dispatchTransactions:c=>this.dispatch(c,this.b)}),this.setupRevertControls(!!e.revertControls,e.revertControls=="b-to-a",e.renderRevertControl),e.parent&&e.parent.appendChild(this.dom),this.scheduleMeasure()}dispatch(e,r){if(e.some(s=>s.docChanged)){let s=e[e.length-1],i=e.reduce((o,a)=>o.compose(a.changes),_t.empty(e[0].startState.doc.length));this.chunks=r==this.a?q.updateA(this.chunks,s.newDoc,this.b.state.doc,i,this.diffConf):q.updateB(this.chunks,this.a.state.doc,s.newDoc,i,this.diffConf),r.update([...e,s.state.update({effects:Ne.of(this.chunks)})]);let t=r==this.a?this.b:this.a;t.update([t.state.update({effects:Ne.of(this.chunks)})]),this.scheduleMeasure()}else r.update(e)}reconfigure(e){if("diffConfig"in e&&(this.diffConf=e.diffConfig),"orientation"in e){let t=e.orientation!="b-a";if(t!=(this.editorDOM.firstChild==this.a.dom.parentNode)){let o=this.a.dom.parentNode,a=this.b.dom.parentNode;o.remove(),a.remove(),this.editorDOM.insertBefore(t?o:a,this.editorDOM.firstChild),this.editorDOM.appendChild(t?a:o),this.revertToLeft=!this.revertToLeft,this.revertDOM&&(this.revertDOM.textContent="")}}if("revertControls"in e||"renderRevertControl"in e){let t=!!this.revertDOM,o=this.revertToA,a=this.renderRevert;"revertControls"in e&&(t=!!e.revertControls,o=e.revertControls=="b-to-a"),"renderRevertControl"in e&&(a=e.renderRevertControl),this.setupRevertControls(t,o,a)}let r="highlightChanges"in e,s="gutter"in e,i="collapseUnchanged"in e;if(r||s||i){let t=[],o=[];if(r||s){let a=this.a.state.facet(V),l=s?e.gutter!==!1:a.markGutter,u=r?e.highlightChanges!==!1:a.highlightChanges;t.push(Ae.reconfigure([V.of({side:"a",sibling:()=>this.b,highlightChanges:u,markGutter:l}),l?Ce:[]])),o.push(Ae.reconfigure([V.of({side:"b",sibling:()=>this.a,highlightChanges:u,markGutter:l}),l?Ce:[]]))}if(i){let a=wt.reconfigure(e.collapseUnchanged?Ge(e.collapseUnchanged):[]);t.push(a),o.push(a)}this.a.dispatch({effects:t}),this.b.dispatch({effects:o})}this.scheduleMeasure()}setupRevertControls(e,r,s){this.revertToA=r,this.revertToLeft=this.revertToA==(this.editorDOM.firstChild==this.a.dom.parentNode),this.renderRevert=s,!e&&this.revertDOM?(this.revertDOM.remove(),this.revertDOM=null):e&&!this.revertDOM?(this.revertDOM=this.editorDOM.insertBefore(document.createElement("div"),this.editorDOM.firstChild.nextSibling),this.revertDOM.addEventListener("mousedown",i=>this.revertClicked(i)),this.revertDOM.className="cm-merge-revert"):this.revertDOM&&(this.revertDOM.textContent="")}scheduleMeasure(){if(this.measuring<0){let e=this.dom.ownerDocument.defaultView||window;this.measuring=e.requestAnimationFrame(()=>{this.measuring=-1,this.measure()})}}measure(){Xn(this.a,this.b,this.chunks),this.revertDOM&&this.updateRevertButtons()}updateRevertButtons(){let e=this.revertDOM,r=e.firstChild,s=this.a.viewport,i=this.b.viewport;for(let t=0;ts.to||o.fromB>i.to)break;if(o.fromA-1&&(this.dom.ownerDocument.defaultView||window).cancelAnimationFrame(this.measuring),this.dom.remove()}}function kt(n){let e=n.nextSibling;return n.remove(),e}const ir=new class extends $e{constructor(){super(...arguments),this.elementClass="cm-deletedLineGutter"}},or=_e.low(Rt({class:"cm-changeGutter",markers:n=>{var e;return((e=n.plugin(Se))===null||e===void 0?void 0:e.gutter)||Lt.empty},widgetMarker:(n,e)=>e instanceof zt?ir:null}));function sr(n){var e;let r=typeof n.original=="string"?Dt.of(n.original.split(/\r?\n/)):n.original,s=n.diffConfig||Ut;return[_e.low(Se),cr,Yt,T.editorAttributes.of({class:"cm-merge-b"}),Ht.of((i,t)=>{let o=t.effects.find(a=>a.is(We));return o&&(i=q.updateA(i,o.value.doc,t.startState.doc,o.value.changes,s)),t.docChanged&&(i=q.updateB(i,t.state.field(oe),t.newDoc,t.changes,s)),i}),V.of({highlightChanges:n.highlightChanges!==!1,markGutter:n.gutter!==!1,syntaxHighlightDeletions:n.syntaxHighlightDeletions!==!1,syntaxHighlightDeletionsMaxLength:3e3,mergeControls:(e=n.mergeControls)!==null&&e!==void 0?e:!0,overrideChunk:n.allowInlineDiffs?mr:void 0,side:"b"}),oe.init(()=>r),n.gutter!==!1?or:[],n.collapseUnchanged?Ge(n.collapseUnchanged):[],j.init(i=>q.build(r,i.doc,s))]}const We=ne.define(),oe=he.define({create:()=>Dt.empty,update(n,e){for(let r of e.effects)r.is(We)&&(n=r.value.doc);return n}}),Ct=new WeakMap;class zt extends De{constructor(e){super(),this.buildDOM=e,this.dom=null}eq(e){return this.dom==e.dom}toDOM(e){return this.dom||(this.dom=this.buildDOM(e))}}function ar(n,e,r){let s=Ct.get(e.changes);if(s)return s;let i=o=>{let{highlightChanges:a,syntaxHighlightDeletions:l,syntaxHighlightDeletionsMaxLength:u,mergeControls:d}=n.facet(V),c=document.createElement("div");if(c.className="cm-deletedChunk",d){let _=c.appendChild(document.createElement("div"));_.className="cm-chunkButtons";let S=A=>{A.preventDefault(),lr(o,o.posAtDOM(c))},B=A=>{A.preventDefault(),dr(o,o.posAtDOM(c))};if(typeof d=="function")_.appendChild(d("accept",S)),_.appendChild(d("reject",B));else{let A=_.appendChild(document.createElement("button"));A.name="accept",A.textContent=n.phrase("Accept"),A.onmousedown=S;let R=_.appendChild(document.createElement("button"));R.name="reject",R.textContent=n.phrase("Reject"),R.onmousedown=B}}if(r||e.fromA>=e.toA)return c;let m=o.state.field(oe).sliceString(e.fromA,e.endA),g=l&&n.facet(xn),p=D(),x=e.changes,v=0,y=!1;function D(){let _=c.appendChild(document.createElement("div"));return _.className="cm-deletedLine",_.appendChild(document.createElement("del"))}function G(_,S,B){for(let A=_;A-1&&YA){let H=document.createTextNode(m.slice(A,R));if(me){let pe=p.appendChild(document.createElement("span"));pe.className=me,pe.appendChild(H)}else p.appendChild(H);A=R}ge&&(y=!y)}}if(g&&e.toA-e.fromA<=u){let _=g.parser.parse(m),S=0;wn(_,{style:B=>kn(n,B)},(B,A,R)=>{B>S&&G(S,B,""),G(B,A,R),S=A}),G(S,m.length,"")}else G(0,m.length,"");return p.firstChild||p.appendChild(document.createElement("br")),c},t=E.widget({block:!0,side:-1,widget:new zt(i)});return Ct.set(e.changes,t),t}function lr(n,e){let{state:r}=n,s=e??r.selection.main.head,i=n.state.field(j).find(l=>l.fromB<=s&&l.endB>=s);if(!i)return!1;let t=n.state.sliceDoc(i.fromB,Math.max(i.fromB,i.toB-1)),o=n.state.field(oe);i.fromB!=i.toB&&i.toA<=o.length&&(t+=n.state.lineBreak);let a=_t.of({from:i.fromA,to:Math.min(o.length,i.toA),insert:t},o.length);return n.dispatch({effects:We.of({doc:a.apply(o),changes:a}),userEvent:"accept"}),!0}function dr(n,e){let{state:r}=n,s=e??r.selection.main.head,i=r.field(j).find(a=>a.fromB<=s&&a.endB>=s);if(!i)return!1;let o=r.field(oe).sliceString(i.fromA,Math.max(i.fromA,i.toA-1));return i.fromA!=i.toA&&i.toB<=r.doc.length&&(o+=r.lineBreak),n.dispatch({changes:{from:i.fromB,to:Math.min(r.doc.length,i.toB),insert:o},userEvent:"revert"}),!0}function yt(n){let e=new re;for(let r of n.field(j)){let s=n.facet(V).overrideChunk&&Xt(n,r);e.add(r.fromB,r.fromB,ar(n,r,!!s))}return e.finish()}const cr=he.define({create:n=>yt(n),update(n,e){return e.state.field(j,!1)!=e.startState.field(j,!1)?yt(e.state):n},provide:n=>T.decorations.from(n)}),Ot=new WeakMap;function Xt(n,e){let r=Ot.get(e);if(r!==void 0)return r;r=null;let s=n.field(oe),i=n.doc,t=s.lineAt(e.endA).number-s.lineAt(e.fromA).number+1,o=i.lineAt(e.endB).number-i.lineAt(e.fromB).number+1;e:if(t==o&&t<10){let a=[],l=0,u=e.fromA,d=e.fromB;for(let c of e.changes){if(c.fromA=e.endB)break;o=n.doc.lineAt(o.to+1)}return!0}const gr=Cn({String:Z.string,Number:Z.number,"True False":Z.bool,PropertyName:Z.propertyName,Null:Z.null,", :":Z.separator,"[ ]":Z.squareBracket,"{ }":Z.brace}),pr=yn.deserialize({version:14,states:"$bOVQPOOOOQO'#Cb'#CbOnQPO'#CeOvQPO'#ClOOQO'#Cr'#CrQOQPOOOOQO'#Cg'#CgO}QPO'#CfO!SQPO'#CtOOQO,59P,59PO![QPO,59PO!aQPO'#CuOOQO,59W,59WO!iQPO,59WOVQPO,59QOqQPO'#CmO!nQPO,59`OOQO1G.k1G.kOVQPO'#CnO!vQPO,59aOOQO1G.r1G.rOOQO1G.l1G.lOOQO,59X,59XOOQO-E6k-E6kOOQO,59Y,59YOOQO-E6l-E6l",stateData:"#O~OeOS~OQSORSOSSOTSOWQO_ROgPO~OVXOgUO~O^[O~PVO[^O~O]_OVhX~OVaO~O]bO^iX~O^dO~O]_OVha~O]bO^ia~O",goto:"!kjPPPPPPkPPkqwPPPPk{!RPPP!XP!e!hXSOR^bQWQRf_TVQ_Q`WRg`QcZRicQTOQZRQe^RhbRYQR]R",nodeNames:"⚠ JsonText True False Null Number String } { Object Property PropertyName : , ] [ Array",maxTerm:25,nodeProps:[["isolate",-2,6,11,""],["openedBy",7,"{",14,"["],["closedBy",8,"}",15,"]"]],propSources:[gr],skippedNodes:[0],repeatNodeCount:2,tokenData:"(|~RaXY!WYZ!W]^!Wpq!Wrs!]|}$u}!O$z!Q!R%T!R![&c![!]&t!}#O&y#P#Q'O#Y#Z'T#b#c'r#h#i(Z#o#p(r#q#r(w~!]Oe~~!`Wpq!]qr!]rs!xs#O!]#O#P!}#P;'S!];'S;=`$o<%lO!]~!}Og~~#QXrs!]!P!Q!]#O#P!]#U#V!]#Y#Z!]#b#c!]#f#g!]#h#i!]#i#j#m~#pR!Q![#y!c!i#y#T#Z#y~#|R!Q![$V!c!i$V#T#Z$V~$YR!Q![$c!c!i$c#T#Z$c~$fR!Q![!]!c!i!]#T#Z!]~$rP;=`<%l!]~$zO]~~$}Q!Q!R%T!R![&c~%YRT~!O!P%c!g!h%w#X#Y%w~%fP!Q![%i~%nRT~!Q![%i!g!h%w#X#Y%w~%zR{|&T}!O&T!Q![&Z~&WP!Q![&Z~&`PT~!Q![&Z~&hST~!O!P%c!Q![&c!g!h%w#X#Y%w~&yO[~~'OO_~~'TO^~~'WP#T#U'Z~'^P#`#a'a~'dP#g#h'g~'jP#X#Y'm~'rOR~~'uP#i#j'x~'{P#`#a(O~(RP#`#a(U~(ZOS~~(^P#f#g(a~(dP#i#j(g~(jP#X#Y(m~(rOQ~~(wOW~~(|OV~",tokenizers:[0],topRules:{JsonText:[0,1]},tokenPrec:0}),vr=An.define({name:"json",parser:pr.configure({props:[Bn.add({Object:it({except:/^\s*\}/}),Array:it({except:/^\s*\]/})}),Mn.add({"Object Array":_n})]}),languageData:{closeBrackets:{brackets:["[","{",'"']},indentOnInput:/^\s*[\}\]]$/}});function br(){return new On(vr)}const xr={class:"space-y-6"},wr={class:"flex items-end justify-between gap-4 flex-wrap"},kr={class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"},Cr={class:"flex items-center gap-2"},yr={class:"pb-5 border-b border-border"},Or={class:"grid gap-3 sm:gap-4 sm:grid-cols-[1fr_auto_1fr] items-end"},Ar={class:"space-y-1.5"},Br=["value"],Mr=["value","disabled"],_r=["disabled"],Dr={class:"space-y-1.5"},Sr=["value"],Rr=["value","disabled"],Lr={key:0,class:"mt-4 flex flex-wrap items-center gap-2 justify-end"},Pr={key:0,class:"text-sm text-foreground-muted py-12 text-center"},Tr={key:1,class:"text-sm text-foreground-muted py-12 text-center"},Er={key:2,class:"bg-warning/10 border border-warning/30 rounded-lg p-5 space-y-3 text-sm"},Qr={key:0,class:"font-mono text-xs space-y-1"},Nr=["title","onClick"],Vr={key:1,class:"text-warning/90"},Gr={key:1,class:"text-warning/70 italic text-xs"},$r={key:3,class:"bg-danger/10 border border-danger/30 rounded-lg p-5 text-sm text-danger"},jr={key:4,class:"bg-danger/10 border border-danger/30 rounded-lg p-5 text-sm text-danger"},Fr={key:0,class:"text-xs text-foreground-muted -mt-2"},Ir={key:1,class:"bg-background border border-border rounded-lg"},Wr={class:"px-4 py-3 flex items-center justify-between gap-3"},Hr={class:"text-xs font-bold uppercase tracking-wider text-foreground-muted flex items-center gap-2"},qr={class:"text-[10px] px-1.5 py-0.5 rounded bg-warning/15 text-warning border border-warning/30 font-medium tracking-normal normal-case"},Ur=["aria-label"],Zr={key:0,class:"px-4 pb-4 pt-3 border-t border-border/60"},Yr={class:"font-mono text-xs space-y-1"},zr={key:2,class:"bg-background border border-border rounded-lg overflow-hidden"},Xr={class:"px-4 py-2.5 flex items-center justify-between gap-3 bg-surface border-b border-border"},Jr={class:"text-xs font-bold uppercase tracking-wider text-white flex items-center gap-2"},Kr={class:"font-mono normal-case tracking-normal text-sm font-medium"},ei={key:0,class:"text-[10px] px-1.5 py-0.5 rounded bg-success/15 text-success border border-success/30 font-medium tracking-normal normal-case"},ti={key:1,class:"text-[10px] px-1.5 py-0.5 rounded bg-danger/15 text-danger border border-danger/30 font-medium tracking-normal normal-case"},ni=["title","aria-pressed"],ri={key:3,class:"bg-background border border-border rounded-lg overflow-hidden"},ii={class:"px-4 py-2.5 flex items-center justify-between gap-3 bg-surface border-b border-border"},oi={class:"text-xs font-bold uppercase tracking-wider text-foreground-muted flex items-center gap-2"},si={class:"font-mono normal-case tracking-normal text-white text-sm font-medium"},ai=["aria-label"],li={key:4,class:"text-sm text-foreground-muted py-12 text-center"},At="orva:diff:sideBySide",xi={__name:"FunctionDiff",setup(n){const e=hn(),r=fn(),s=$(()=>e.params.name),i=$(()=>e.query.from||""),t=$(()=>e.query.to||""),o=P(null),a=P([]),l=P(null),u=P(""),d=P(""),c=P([]),m=P("Copy link"),g=P(!0),p=P(!0),x=P(!1),v=sn(),y=P(!0),D=P(null),G=$(()=>D.value===null?y.value:D.value);try{const h=typeof window<"u"&&window.localStorage?.getItem?.(At);h==="true"?D.value=!0:h==="false"&&(D.value=!1)}catch{}const _=()=>{const h=!G.value;D.value=h;try{window.localStorage?.setItem?.(At,String(h))}catch{}ve()},S=P(null),B=P(null);let A=null,R=null;const me=$(()=>o.value?.runtime?.startsWith("python")?[Rn()]:[Ln()]),ge=T.theme({"&.cm-merge-a .cm-changedLine":{backgroundColor:"rgba(248, 81, 73, 0.15)"},"&.cm-merge-b .cm-changedLine":{backgroundColor:"rgba(63, 185, 80, 0.15)"},"&.cm-merge-a .cm-changedText":{background:"none",backgroundColor:"rgba(248, 81, 73, 0.35)",borderRadius:"2px"},"&.cm-merge-b .cm-changedText":{background:"none",backgroundColor:"rgba(63, 185, 80, 0.30)",borderRadius:"2px"},"&.cm-merge-a .cm-changedLineGutter":{color:"#f85149"},"&.cm-merge-b .cm-changedLineGutter":{color:"#3fb950"},".cm-deletedChunk":{backgroundColor:"rgba(248, 81, 73, 0.15)"},".cm-deletedChunk .cm-deletedText":{background:"none",backgroundColor:"rgba(248, 81, 73, 0.35)",borderRadius:"2px"},".cm-deletedChunk .cm-changedLineGutter, .cm-deletedChunk .cm-deletedLineGutter":{color:"#f85149"},".cm-insertedLine, .cm-changedLine, .cm-inlineChangedLine":{backgroundColor:"rgba(63, 185, 80, 0.15)"},".cm-changedLine .cm-changedText, .cm-inlineChangedLine .cm-changedText":{background:"none",backgroundColor:"rgba(63, 185, 80, 0.30)",borderRadius:"2px"},".cm-changedLineGutter, .cm-inlineChangedLineGutter":{color:"#3fb950"}}),Y=h=>[Dn,...h,Sn,ge,Be.readOnly.of(!0),T.lineWrapping];let H=null;const pe=()=>{if(typeof window>"u"||!window.matchMedia)return;const h=window.matchMedia("(min-width: 768px)"),f=O=>{const b=!!O.matches;b!==y.value&&(y.value=b,D.value===null&&ve())};f(h),h.addEventListener?(h.addEventListener("change",f),H=()=>h.removeEventListener("change",f)):(h.addListener(f),H=()=>h.removeListener(f))},He=(h,f,O,b)=>h?G.value?new rr({a:{doc:f,extensions:Y(b)},b:{doc:O,extensions:Y(b)},parent:h,orientation:"a-b",revertControls:!1,highlightChanges:!0,gutter:!0}):new T({parent:h,state:Be.create({doc:O,extensions:[...Y(b),sr({original:f,mergeControls:!1})]})}):null,qe=()=>{R?.destroy?.(),R=null},Re=()=>{A?.destroy?.(),qe(),A=null},Ue=()=>{const h=U.value;!h||!p.value||h.before===h.after&&!h.added&&!h.removed||(R=He(B.value,h.before||"",h.after||"",[br()]))},ve=async()=>{if(await nt(),Re(),!l.value)return;const h=K.value;h&&(A=He(S.value,h.before||"",h.after||"",me.value)),Ue()},Ze=async()=>{if(u.value="",d.value="",l.value=null,Re(),!(!o.value||!i.value||!t.value||i.value===t.value))try{const h=await cn(o.value.id,i.value,t.value,"json");l.value=h.data,await ve()}catch(h){const f=h.response?.data?.error;u.value=f?.code||"INTERNAL",d.value=f?.message||h.message||"Failed to load diff.",u.value==="VERSION_GCD"&&(c.value=f?.details?.available_hashes||[])}},K=$(()=>l.value?.files?.find(h=>h.kind==="handler")),U=$(()=>l.value?.files?.find(h=>h.kind==="manifest")),be=$(()=>{const h=l.value?.from?.snapshot,f=l.value?.to?.snapshot;return!h||!f?[]:rt(h,f)}),Jt=h=>h.startsWith("+")?"text-success":h.startsWith("-")?"text-danger":"text-warning",Ye=$(()=>{const h=o.value?.code_hash||"";return a.value.filter(f=>f.status==="succeeded"&&f.code_hash).map(f=>({id:f.id,version:f.version,shortHash:(f.code_hash||"").slice(0,12),submittedAt:f.submitted_at,isActive:!!h&&f.code_hash===h}))}),ze=h=>{if(!h)return"";const f=new Date(h),O=f.toLocaleDateString(void 0,{month:"numeric",day:"numeric"}),b=f.toLocaleTimeString(void 0,{hour:"numeric",minute:"2-digit"});return`${O} ${b}`},Xe=h=>`v${h.version} · ${h.shortHash} · ${ze(h.submittedAt)}${h.isActive?" · active":""}`,ee=({from:h,to:f})=>{r.replace({query:{from:h??i.value,to:f??t.value}})},Kt=()=>{!i.value||!t.value||r.replace({query:{from:t.value,to:i.value}})},en=async()=>{const h=await pn(window.location.href);m.value=h?"Copied!":"Copy failed",setTimeout(()=>{m.value="Copy link"},1500)},se=$(()=>{const h=o.value?.code_hash;return h&&a.value.find(O=>O.status==="succeeded"&&O.code_hash===h)?.id||null}),te=$(()=>a.value.find(h=>h.id===i.value)||null),Je=$(()=>!!(te.value&&o.value?.code_hash&&te.value.code_hash===o.value.code_hash)),tn=()=>{if(!se.value||!o.value)return;const h=o.value.code_hash,O=a.value.filter(b=>b.status==="succeeded"&&b.code_hash&&b.code_hash!==h)[0];ee({from:O?.id||i.value,to:se.value})},ae=h=>a.value.find(f=>f.code_hash===h)||null,nn=h=>{const f=ae(h);if(!f)return;const O=a.value.find(b=>b.id===t.value);O&&c.value.length&&!c.value.includes(O.code_hash)?ee({from:f.id,to:se.value||t.value}):ee({from:f.id,to:t.value})},rn=async()=>{try{const f=((await Ke()).data?.functions||[]).find(b=>b.name===s.value);f&&(o.value=f);const O=await et(o.value.id,100);a.value=O.data.deployments||O.data||[]}catch{}},on=async()=>{if(x.value||!o.value||!te.value||Je.value)return;const h=te.value,f=(h.code_hash||"").slice(0,12);let O=`Code hash ${f}. The current version stays in history.`;try{const z=(await mn(h.id))?.data?.snapshot;if(z){const le=rt(o.value,z);le.length?O=`Rolling back to v${h.version} (code ${f}) will also change: + +${le.join(` +`)} + +Secrets keep their current values; they aren't part of the rollback.`:O=`Rolling back to v${h.version} (code ${f}). Settings and env are already identical, so only the code changes.`}}catch{}if(await v.ask({title:`Restore v${h.version}?`,message:O,confirmLabel:"Rollback"})){x.value=!0;try{const F=await gn(o.value.id,{deployment_id:h.id}),z=F?.data?.id||F?.data?.deployment_id;await rn(),z&&ee({from:i.value,to:z}),v.notify({title:"Rollback complete",message:`v${h.version} (code ${f}) is now serving.`})}catch(F){const z=F.response?.data?.error?.code||"",le=F.response?.data?.error?.message||F.message||"Rollback failed";z==="VERSION_GCD"?v.notify({title:"Version unavailable",message:`This version has been garbage-collected and can no longer be restored. + +${le}`,danger:!0}):v.notify({title:"Rollback failed",message:le,danger:!0})}finally{x.value=!1}}};return an(async()=>{pe();try{const f=((await Ke()).data?.functions||[]).find(O=>O.name===s.value);if(!f)throw new Error("not found");o.value=f}catch(h){u.value=h.response?.data?.error?.code||"FUNCTION_NOT_FOUND",d.value=h.response?.data?.error?.message||"Function not found.";return}try{const h=await et(o.value.id,100);a.value=h.data.deployments||h.data||[]}catch{a.value=[]}await Ze()}),Le(p,async h=>{await nt(),qe(),h&&Ue()}),Le([i,t],Ze),Le(()=>o.value?.runtime,()=>{ve()}),ln(()=>{Re(),H?.()}),(h,f)=>{const O=un("router-link");return k(),C("div",xr,[w("div",wr,[w("div",null,[f[7]||(f[7]=w("h1",{class:"text-xl font-semibold text-white tracking-tight text-balance"}," Compare versions ",-1)),w("p",kr,[f[5]||(f[5]=Q(" Source diff between two past deployments of ",-1)),N(O,{to:`/functions/${s.value}`,class:"text-white underline"},{default:xe(()=>[Q(L(s.value),1)]),_:1},8,["to"]),f[6]||(f[6]=Q(". ",-1))])]),w("div",Cr,[w("button",{class:"text-xs text-foreground-muted hover:text-white hover:bg-surface-hover rounded-md flex items-center justify-center gap-1.5 px-2 py-1.5 transition-colors min-w-[6rem] focus:outline-none focus-visible:ring-1 focus-visible:ring-white",title:"Copy share link","aria-label":"Copy share link",onClick:en},[N(I(Pn),{class:"w-3.5 h-3.5"}),Q(" "+L(m.value),1)]),N(tt,{variant:"secondary",onClick:f[0]||(f[0]=b=>h.$router.push(`/functions/${s.value}/deployments`))},{default:xe(()=>[N(I($n),{class:"w-4 h-4 mr-2"}),f[8]||(f[8]=Q(" All deployments ",-1))]),_:1})])]),w("div",yr,[w("div",Or,[w("div",Ar,[f[10]||(f[10]=w("label",{class:"block text-[10px] font-bold uppercase tracking-label text-foreground-muted"},[Q(" From "),w("span",{class:"ml-1 text-foreground-muted/60 normal-case font-medium tracking-normal"},"(older)")],-1)),w("select",{value:i.value,class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm font-mono text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onChange:f[1]||(f[1]=b=>ee({from:b.target.value}))},[f[9]||(f[9]=w("option",{value:"",disabled:""}," Pick a version ",-1)),(k(!0),C(de,null,we(Ye.value,b=>(k(),C("option",{key:b.id,value:b.id,disabled:b.id===t.value},L(Xe(b)),9,Mr))),128))],40,Br)]),w("button",{disabled:!i.value||!t.value,class:"justify-self-center sm:self-end mb-1 h-9 w-9 flex items-center justify-center rounded-md text-foreground-muted hover:text-white hover:bg-surface-hover transition-colors disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-foreground-muted",title:"Swap from / to","aria-label":"Swap from and to versions",onClick:Kt},[N(I(Gn),{class:"w-4 h-4 rotate-90 sm:rotate-0 motion-reduce:transition-none transition-transform"})],8,_r),w("div",Dr,[f[12]||(f[12]=w("label",{class:"block text-[10px] font-bold uppercase tracking-label text-foreground-muted"},[Q(" To "),w("span",{class:"ml-1 text-foreground-muted/60 normal-case font-medium tracking-normal"},"(newer)")],-1)),w("select",{value:t.value,class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm font-mono text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onChange:f[2]||(f[2]=b=>ee({to:b.target.value}))},[f[11]||(f[11]=w("option",{value:"",disabled:""}," Pick a version ",-1)),(k(!0),C(de,null,we(Ye.value,b=>(k(),C("option",{key:b.id,value:b.id,disabled:b.id===i.value},L(Xe(b)),9,Rr))),128))],40,Sr)])]),o.value?(k(),C("div",Lr,[se.value&&t.value!==se.value?(k(),C("button",{key:0,class:"inline-flex items-center gap-1.5 text-xs text-foreground-muted hover:text-white hover:bg-surface-hover rounded-md px-2.5 py-1.5 transition-colors",title:"Compare previous version with currently active",onClick:tn},[N(I(Tn),{class:"w-3.5 h-3.5"}),f[13]||(f[13]=Q(" vs active ",-1))])):W("",!0),te.value&&!Je.value?(k(),dn(tt,{key:1,variant:"primary",size:"sm",loading:x.value,disabled:x.value,onClick:on},{default:xe(()=>[N(I(Vn),{class:"w-3.5 h-3.5 mr-1.5"}),Q(" Roll back to v"+L(te.value.version),1)]),_:1},8,["loading","disabled"])):W("",!0)])):W("",!0)]),!i.value||!t.value?(k(),C("p",Pr," Pick two different deployments above to compare. ")):i.value===t.value?(k(),C("p",Tr," Same version on both sides. Pick a different deployment to see a diff. ")):u.value==="VERSION_GCD"?(k(),C("div",Er,[f[14]||(f[14]=w("div",{class:"text-warning font-medium"}," Source data for one or both versions has been garbage-collected. ",-1)),f[15]||(f[15]=w("p",{class:"text-warning/80 leading-body"}," The deployment row still exists, but the on-disk code tree was pruned by the version GC. Pick a version whose source is still archived: ",-1)),c.value.length?(k(),C("ul",Qr,[(k(!0),C(de,null,we(c.value,b=>(k(),C("li",{key:b},[ae(b)?(k(),C("button",{key:0,class:"text-left text-warning hover:text-white hover:bg-warning/15 rounded px-1.5 py-0.5 -mx-1.5 transition-colors",title:`Use v${ae(b).version} as the From side`,onClick:F=>nn(b)}," v"+L(ae(b).version)+" · "+L(b.slice(0,12))+"… · "+L(ze(ae(b).submitted_at)),9,Nr)):(k(),C("span",Vr,L(b.slice(0,12))+"…",1))]))),128))])):(k(),C("p",Gr," No surviving on-disk versions for this function. Redeploy the original source to compare against the current code. "))])):u.value==="VERSION_NOT_FOUND"?(k(),C("div",$r,[f[17]||(f[17]=Q(" One of the supplied deployment IDs doesn't exist. Pick from the dropdowns above, or go back to the ",-1)),N(O,{to:`/functions/${s.value}/deployments`,class:"underline"},{default:xe(()=>[...f[16]||(f[16]=[Q(" deployment history ",-1)])]),_:1},8,["to"]),f[18]||(f[18]=Q(". ",-1))])):u.value&&u.value!=="OK"?(k(),C("div",jr,L(d.value||"Failed to load diff."),1)):l.value?(k(),C(de,{key:5},[be.value.length?(k(),C("section",Ir,[w("header",Wr,[w("h2",Hr,[N(I(En),{class:"w-3.5 h-3.5"}),f[19]||(f[19]=Q(" Settings & env ",-1)),w("span",qr,L(be.value.length)+" change"+L(be.value.length===1?"":"s"),1)]),w("button",{class:"text-foreground-muted hover:text-white","aria-label":g.value?"Collapse settings diff":"Expand settings diff",onClick:f[3]||(f[3]=b=>g.value=!g.value)},[N(I(ot),{class:Pe(["w-4 h-4 transition-transform motion-reduce:transition-none",{"rotate-180":g.value}])},null,8,["class"])],8,Ur)]),g.value?(k(),C("div",Zr,[w("ul",Yr,[(k(!0),C(de,null,we(be.value,(b,F)=>(k(),C("li",{key:F,class:Pe(Jt(b))},L(b),3))),128))]),f[20]||(f[20]=w("p",{class:"text-[11px] text-foreground-muted/70 mt-3"}," Secrets aren't tracked per-version; they always reflect the current values. ",-1))])):W("",!0)])):(k(),C("p",Fr," Settings and env are identical between these versions. Secrets aren't tracked per-version. ")),K.value?(k(),C("section",zr,[w("header",Xr,[w("h2",Jr,[N(I(Qn),{class:"w-3.5 h-3.5 text-foreground-muted"}),w("span",Kr,L(K.value.path),1),K.value.added?(k(),C("span",ei,"added")):K.value.removed?(k(),C("span",ti,"removed")):W("",!0)]),w("button",{class:"text-[10px] font-medium uppercase tracking-label text-foreground-muted hover:text-white hover:bg-surface-hover rounded px-2 py-1 transition-colors",title:G.value?"Switch to unified inline view":"Switch to side-by-side view","aria-pressed":G.value,onClick:_},L(G.value?"side-by-side":"unified"),9,ni)]),w("div",{ref_key:"codeMountRef",ref:S,class:"orva-merge"},null,512)])):W("",!0),U.value&&(U.value.before!==U.value.after||U.value.added||U.value.removed)?(k(),C("section",ri,[w("header",ii,[w("h2",oi,[N(I(Nn),{class:"w-3.5 h-3.5"}),w("span",si,L(U.value.path),1),f[21]||(f[21]=w("span",{class:"text-[10px] px-1.5 py-0.5 rounded bg-warning/15 text-warning border border-warning/30 font-medium tracking-normal normal-case"}," changed ",-1))]),w("button",{class:"text-foreground-muted hover:text-white","aria-label":p.value?"Collapse manifest diff":"Expand manifest diff",onClick:f[4]||(f[4]=b=>p.value=!p.value)},[N(I(ot),{class:Pe(["w-4 h-4 transition-transform motion-reduce:transition-none",{"rotate-180":p.value}])},null,8,["class"])],8,ai)]),p.value?(k(),C("div",{key:0,ref_key:"manifestMountRef",ref:B,class:"orva-merge"},null,512)):W("",!0)])):W("",!0),!K.value&&!U.value?(k(),C("p",li," No code changes between these versions. ")):W("",!0)],64)):W("",!0)])}}};export{xi as default}; diff --git a/backend/internal/server/ui_dist/assets/FunctionDiff-dkl8Wiv4.js b/backend/internal/server/ui_dist/assets/FunctionDiff-dkl8Wiv4.js deleted file mode 100644 index ee9389f..0000000 --- a/backend/internal/server/ui_dist/assets/FunctionDiff-dkl8Wiv4.js +++ /dev/null @@ -1,10 +0,0 @@ -import{c as Bt,C as sn,o as an,H as Ke,ab as et,L as Le,y as ln,a as C,b as w,k as Q,d as N,h as xe,f as I,t as L,_ as tt,F as de,p as we,g as W,n as dn,s as Pe,r as P,q as $,ae as cn,U as un,af as nt,i as fn,a1 as hn,j as k,ac as mn,a9 as gn}from"./index-WXhXpu06.js";import{d as rt}from"./rollbackDiff-Cvt2Ss82.js";import{c as pn}from"./clipboard-CmSw2rR-.js";import{P as _e,E as T,a as Be,C as Mt,S as ne,b as _t,T as Dt,c as vn,d as he,D as E,V as bn,F as St,g as Rt,R as re,W as De,G as $e,l as xn,h as wn,e as Lt,f as kn,s as Cn,t as Z,L as yn,i as On,j as An,k as Bn,m as it,n as Mn,o as _n,p as Dn,q as Sn,r as Rn,u as Ln}from"./index-BOWx3BJu.js";import{C as Pn}from"./copy-HWkx-vox.js";import{Z as Tn}from"./zap-C7BVpopq.js";import{S as En,F as Qn,P as Nn}from"./settings-2-2knNMPAW.js";import{C as ot}from"./chevron-down-CtdSqW4P.js";import{R as Vn}from"./rotate-ccw-Cx2X0X0k.js";const Gn=Bt("arrow-left-right",[["path",{d:"M8 3 4 7l4 4",key:"9rb6wj"}],["path",{d:"M4 7h16",key:"6tx8e3"}],["path",{d:"m16 21 4-4-4-4",key:"siv7j2"}],["path",{d:"M20 17H4",key:"h6l3hr"}]]);const $n=Bt("list",[["path",{d:"M3 5h.01",key:"18ugdj"}],["path",{d:"M3 12h.01",key:"nlz23k"}],["path",{d:"M3 19h.01",key:"noohij"}],["path",{d:"M8 5h13",key:"1pao27"}],["path",{d:"M8 12h13",key:"1za7za"}],["path",{d:"M8 19h13",key:"m83p4d"}]]);class M{constructor(e,r,s,i){this.fromA=e,this.toA=r,this.fromB=s,this.toB=i}offset(e,r=e){return new M(this.fromA+e,this.toA+e,this.fromB+r,this.toB+r)}}function J(n,e,r,s,i,t){if(n==s)return[];let o=je(n,e,r,s,i,t),a=Ie(n,e+o,r,s,i+o,t);e+=o,r-=a,i+=o,t-=a;let l=r-e,u=t-i;if(!l||!u)return[new M(e,r,i,t)];if(l>u){let c=n.slice(e,r).indexOf(s.slice(i,t));if(c>-1)return[new M(e,e+c,i,i),new M(e+c+u,r,t,t)]}else if(u>l){let c=s.slice(i,t).indexOf(n.slice(e,r));if(c>-1)return[new M(e,e,i,i+c),new M(r,r,i+c+l,t)]}if(l==1||u==1)return[new M(e,r,i,t)];let d=Et(n,e,r,s,i,t);if(d){let[c,m,g]=d;return J(n,e,c,s,i,m).concat(J(n,c+g,r,s,m+g,t))}return Fn(n,e,r,s,i,t)}let ce=1e9,ue=0,Fe=!1;function Fn(n,e,r,s,i,t){let o=r-e,a=t-i;if(ce<1e9&&Math.min(o,a)>ce*16||ue>0&&Date.now()>ue)return Math.min(o,a)>ce*64?[new M(e,r,i,t)]:st(n,e,r,s,i,t);let l=Math.ceil((o+a)/2);Te.reset(l),Ee.reset(l);let u=(g,p)=>n.charCodeAt(e+g)==s.charCodeAt(i+p),d=(g,p)=>n.charCodeAt(r-g-1)==s.charCodeAt(t-p-1),c=(o-a)%2!=0?Ee:null,m=c?null:Te;for(let g=0;gce||ue>0&&!(g&63)&&Date.now()>ue)return st(n,e,r,s,i,t);let p=Te.advance(g,o,a,l,c,!1,u)||Ee.advance(g,o,a,l,m,!0,d);if(p)return jn(n,e,r,e+p[0],s,i,t,i+p[1])}return[new M(e,r,i,t)]}class Pt{constructor(){this.vec=[]}reset(e){this.len=e<<1;for(let r=0;rr)this.end+=2;else if(c>s)this.start+=2;else if(t){let m=i+(r-s)-l;if(m>=0&&m=r-d)return[g,i+g-m]}else{let g=r-t.vec[m];if(d>=g)return[d,c]}}}return null}}const Te=new Pt,Ee=new Pt;function jn(n,e,r,s,i,t,o,a){let l=!1;return!ie(n,s)&&++s==r&&(l=!0),!ie(i,a)&&++a==o&&(l=!0),l?[new M(e,r,t,o)]:J(n,e,s,i,t,a).concat(J(n,s,r,i,a,o))}function Tt(n,e){let r=1,s=Math.min(n,e);for(;rr||d>t||n.slice(a,u)!=s.slice(l,d)){if(o==1)return a-e-(ie(n,a)?0:1);o=o>>1}else{if(u==r||d==t)return u-e;a=u,l=d}}}function Ie(n,e,r,s,i,t){if(e==r||i==t||n.charCodeAt(r-1)!=s.charCodeAt(t-1))return 0;let o=Tt(r-e,t-i);for(let a=r,l=t;;){let u=a-o,d=l-o;if(u>1}else{if(u==e||d==i)return r-u;a=u,l=d}}}function Qe(n,e,r,s,i,t,o,a){let l=s.slice(i,t),u=null;for(;;){if(u||o=r)break;let m=n.slice(d,c),g=-1;for(;(g=l.indexOf(m,g+1))!=-1;){let p=je(n,c,r,s,i+g+m.length,t),x=Ie(n,e,d,s,i,i+g),v=m.length+p+x;(!u||u[2]>1}}function Et(n,e,r,s,i,t){let o=r-e,a=t-i;if(oi.fromA-e&&s.toB>i.fromB-e&&(n[r-1]=new M(s.fromA,i.toA,s.fromB,i.toB),n.splice(r--,1))}}function In(n,e,r){for(;;){Qt(r,1);let s=!1;for(let i=0;i3||a>3){let l=i==n.length-1?e.length:n[i+1].fromA,u=t.fromA-s,d=l-t.toA,c=lt(e,t.fromA,u),m=at(e,t.toA,d),g=t.fromA-c,p=m-t.toA;if((!o||!a)&&g&&p){let x=Math.max(o,a),[v,y,D]=o?[e,t.fromA,t.toA]:[r,t.fromB,t.toB];x>g&&e.slice(c,t.fromA)==v.slice(D-g,D)?(t=n[i]=new M(c,c+o,t.fromB-g,t.toB-g),c=t.fromA,m=at(e,t.toA,l-t.toA)):x>p&&e.slice(t.toA,m)==v.slice(y,y+p)&&(t=n[i]=new M(m-o,m,t.fromB+p,t.toB+p),m=t.toA,c=lt(e,t.fromA,t.fromA-s)),g=t.fromA-c,p=m-t.toA}if(g||p)t=n[i]=new M(t.fromA-g,t.toA+p,t.fromB-g,t.toB+p);else if(o){if(!a){let x=ct(e,t.fromA,t.toA),v,y=x<0?-1:dt(e,t.toA,t.fromA);x>-1&&(v=x-t.fromA)<=d&&e.slice(t.fromA,x)==e.slice(t.toA,t.toA+v)?t=n[i]=t.offset(v):y>-1&&(v=t.toA-y)<=u&&e.slice(t.fromA-v,t.fromA)==e.slice(y,t.toA)&&(t=n[i]=t.offset(-v))}}else{let x=ct(r,t.fromB,t.toB),v,y=x<0?-1:dt(r,t.toB,t.fromB);x>-1&&(v=x-t.fromB)<=d&&r.slice(t.fromB,x)==r.slice(t.toB,t.toB+v)?t=n[i]=t.offset(v):y>-1&&(v=t.toB-y)<=u&&r.slice(t.fromB-v,t.fromB)==r.slice(y,t.toB)&&(t=n[i]=t.offset(-v))}}s=t.toA}return Qt(n,3),n}let X;try{X=new RegExp("[\\p{Alphabetic}\\p{Number}]","u")}catch{}function Nt(n){return n>48&&n<58||n>64&&n<91||n>96&&n<123}function Vt(n,e){if(e==n.length)return 0;let r=n.charCodeAt(e);return r<192?Nt(r)?1:0:X?!Ft(r)||e==n.length-1?X.test(String.fromCharCode(r))?1:0:X.test(n.slice(e,e+2))?2:0:0}function Gt(n,e){if(!e)return 0;let r=n.charCodeAt(e-1);return r<192?Nt(r)?1:0:X?!jt(r)||e==1?X.test(String.fromCharCode(r))?1:0:X.test(n.slice(e-2,e))?2:0:0}const $t=8;function at(n,e,r){if(e==n.length||!Gt(n,e))return e;for(let s=e,i=e+r,t=0;t<$t;t++){let o=Vt(n,s);if(!o||s+o>i)return s;s+=o}return e}function lt(n,e,r){if(!e||!Vt(n,e))return e;for(let s=e,i=e-r,t=0;t<$t;t++){let o=Gt(n,s);if(!o||s-on>=55296&&n<=56319,jt=n=>n>=56320&&n<=57343;function ie(n,e){return!e||e==n.length||!Ft(n.charCodeAt(e-1))||!jt(n.charCodeAt(e))}function Hn(n,e,r){var s;let i=r?.override;return i?i(n,e):(ce=((s=r?.scanLimit)!==null&&s!==void 0?s:1e9)>>1,ue=r?.timeout?Date.now()+r.timeout:0,Fe=!1,In(n,e,J(n,0,n.length,e,0,e.length)))}function It(){return!Fe}function Wt(n,e,r){return Wn(Hn(n,e,r),n,e)}const V=St.define({combine:n=>n[0]}),Ne=ne.define(),Ht=St.define(),F=he.define({create(n){return null},update(n,e){for(let r of e.effects)r.is(Ne)&&(n=r.value);for(let r of e.state.facet(Ht))n=r(n,e);return n}});class q{constructor(e,r,s,i,t,o=!0){this.changes=e,this.fromA=r,this.toA=s,this.fromB=i,this.toB=t,this.precise=o}offset(e,r){return e||r?new q(this.changes,this.fromA+e,this.toA+e,this.fromB+r,this.toB+r,this.precise):this}get endA(){return Math.max(this.fromA,this.toA-1)}get endB(){return Math.max(this.fromB,this.toB-1)}static build(e,r,s){let i=Wt(e.toString(),r.toString(),s);return qt(i,e,r,0,0,It())}static updateA(e,r,s,i,t){return gt(mt(e,i,!0,s.length),e,r,s,t)}static updateB(e,r,s,i,t){return gt(mt(e,i,!1,r.length),e,r,s,t)}}function ut(n,e,r,s){let i=r.lineAt(n),t=s.lineAt(e);return i.to==n&&t.to==e&&nc+1&&v>m+1)break;g.push(p.offset(-u+s,-d+i)),[c,m]=ft(p.toA+s,p.toB+i,e,r),a++}o.push(new q(g,u,Math.max(u,c),d,Math.max(d,m),t))}return o}const ke=1e3;function ht(n,e,r,s){let i=0,t=n.length;for(;;){if(i==t){let d=0,c=0;i&&({toA:d,toB:c}=n[i-1]);let m=e-(r?d:c);return[d+m,c+m]}let o=i+t>>1,a=n[o],[l,u]=r?[a.fromA,a.toA]:[a.fromB,a.toB];if(l>e)t=o;else if(u<=e)i=o+1;else return s?[a.fromA,a.fromB]:[a.toA,a.toB]}}function mt(n,e,r,s){let i=[];return e.iterChangedRanges((t,o,a,l)=>{let u=0,d=r?e.length:s,c=0,m=r?s:e.length;t>ke&&([u,c]=ht(n,t-ke,r,!0)),o=u?i[i.length-1]={fromA:p.fromA,fromB:p.fromB,toA:d,toB:m,diffA:p.diffA+x,diffB:p.diffB+v}:i.push({fromA:u,toA:d,fromB:c,toB:m,diffA:x,diffB:v})}),i}function gt(n,e,r,s,i){if(!n.length)return e;let t=[];for(let o=0,a=0,l=0,u=0;;o++){let d=o==n.length?null:n[o],c=d?d.fromA+a:r.length,m=d?d.fromB+l:s.length;for(;uc||v.toB+l>m))break;t.push(v.offset(a,l)),u++}if(!d)break;let g=d.toA+a+d.diffA,p=d.toB+l+d.diffB,x=Wt(r.sliceString(c,g),s.sliceString(m,p),i);for(let v of qt(x,r,s,c,m,It()))t.push(v);for(a+=d.diffA,l+=d.diffB;ug&&v.fromB+l>p)break;u++}}return t}const Ut={scanLimit:500},Se=bn.fromClass(class{constructor(n){({deco:this.deco,gutter:this.gutter}=bt(n))}update(n){(n.docChanged||n.viewportChanged||qn(n.startState,n.state)||Un(n.startState,n.state))&&({deco:this.deco,gutter:this.gutter}=bt(n.view))}},{decorations:n=>n.deco}),Ce=_e.low(Rt({class:"cm-changeGutter",markers:n=>{var e;return((e=n.plugin(Se))===null||e===void 0?void 0:e.gutter)||Lt.empty}}));function qn(n,e){return n.field(F,!1)!=e.field(F,!1)}function Un(n,e){return n.facet(V)!=e.facet(V)}const pt=E.line({class:"cm-changedLine"}),Zt=E.mark({class:"cm-changedText"}),Zn=E.mark({tagName:"ins",class:"cm-insertedLine"}),Yn=E.mark({tagName:"del",class:"cm-deletedLine"}),vt=new class extends $e{constructor(){super(...arguments),this.elementClass="cm-changedLineGutter"}};function zn(n,e,r,s,i,t){let o=r?n.fromA:n.fromB,a=r?n.toA:n.toB,l=0;if(o!=a){i.add(o,o,pt),i.add(o,a,r?Yn:Zn),t&&t.add(o,o,vt);for(let u=e.iterRange(o,a-1),d=o;!u.next().done;){if(u.lineBreak){d++,i.add(d,d,pt),t&&t.add(d,d,vt);continue}let c=d+u.value.length;if(s)for(;l=d)break;(o?c.toA:c.toB)>u&&(!t||!t(n.state,c,a,l))&&zn(c,n.state.doc,o,s,a,l)}return{deco:a.finish(),gutter:l&&l.finish()}}class ye extends De{constructor(e){super(),this.height=e}eq(e){return this.height==e.height}toDOM(){let e=document.createElement("div");return e.className="cm-mergeSpacer",e.style.height=this.height+"px",e}updateDOM(e){return e.style.height=this.height+"px",!0}get estimatedHeight(){return this.height}ignoreEvent(){return!1}}const Me=ne.define({map:(n,e)=>n.map(e)}),fe=he.define({create:()=>E.none,update:(n,e)=>{for(let r of e.effects)if(r.is(Me))return r.value;return n.map(e.changes)},provide:n=>T.decorations.from(n)}),Oe=.01;function xt(n,e){if(n.size!=e.size)return!1;let r=n.iter(),s=e.iter();for(;r.value;){if(r.from!=s.from||Math.abs(r.value.spec.widget.height-s.value.spec.widget.height)>1)return!1;r.next(),s.next()}return!0}function Xn(n,e,r){let s=new re,i=new re,t=n.state.field(fe).iter(),o=e.state.field(fe).iter(),a=0,l=0,u=0,d=0,c=n.viewport,m=e.viewport;for(let v=0;;v++){let y=vOe&&(d+=B,i.add(l,l,E.widget({widget:new ye(B),block:!0,side:-1})))}if(D>a+1e3&&ac.from&&lm.from){let _=Math.min(c.from-a,m.from-l);a+=_,l+=_,v--}else if(y)a=y.toA,l=y.toB;else break;for(;t.value&&t.fromOe&&i.add(e.state.doc.length,e.state.doc.length,E.widget({widget:new ye(g),block:!0,side:1}));let p=s.finish(),x=i.finish();xt(p,n.state.field(fe))||n.dispatch({effects:Me.of(p)}),xt(x,e.state.field(fe))||e.dispatch({effects:Me.of(x)})}const Ve=ne.define({map:(n,e)=>e.mapPos(n)});class Jn extends De{constructor(e){super(),this.lines=e}eq(e){return this.lines==e.lines}toDOM(e){let r=document.createElement("div");return r.className="cm-collapsedLines",r.textContent=e.state.phrase("$ unchanged lines",this.lines),r.addEventListener("click",s=>{let i=e.posAtDOM(s.target);e.dispatch({effects:Ve.of(i)});let{side:t,sibling:o}=e.state.facet(V);o&&o().dispatch({effects:Ve.of(Kn(i,e.state.field(F),t=="a"))})}),r}ignoreEvent(e){return e instanceof MouseEvent}get estimatedHeight(){return 27}get type(){return"collapsed-unchanged-code"}}function Kn(n,e,r){let s=0,i=0;for(let t=0;;t++){let o=t=n)return i+(n-s);[s,i]=r?[o.toA,o.toB]:[o.toB,o.toA]}}const er=he.define({create(n){return E.none},update(n,e){n=n.map(e.changes);for(let r of e.effects)r.is(Ve)&&(n=n.update({filter:s=>s!=r.value}));return n},provide:n=>T.decorations.from(n)});function Ge({margin:n=3,minSize:e=4}){return er.init(r=>tr(r,n,e))}function tr(n,e,r){let s=new re,i=n.facet(V).side=="a",t=n.field(F),o=1;for(let a=0;;a++){let l=a=r&&s.add(n.doc.line(u).from,n.doc.line(d).to,E.replace({widget:new Jn(c),block:!0})),!l)break;o=n.doc.lineAt(Math.min(n.doc.length,i?l.toA:l.toB)).number}return s.finish()}const nr=T.styleModule.of(new vn({".cm-mergeView":{overflowY:"auto"},".cm-mergeViewEditors":{display:"flex",alignItems:"stretch"},".cm-mergeViewEditor":{flexGrow:1,flexBasis:0,overflow:"hidden"},".cm-merge-revert":{width:"1.6em",flexGrow:0,flexShrink:0,position:"relative"},".cm-merge-revert button":{position:"absolute",display:"block",width:"100%",boxSizing:"border-box",textAlign:"center",background:"none",border:"none",font:"inherit",cursor:"pointer"}})),Yt=T.baseTheme({".cm-mergeView & .cm-scroller, .cm-mergeView &":{height:"auto !important",overflowY:"visible !important"},"&.cm-merge-a .cm-changedLine, .cm-deletedChunk":{backgroundColor:"rgba(160, 128, 100, .08)"},"&.cm-merge-b .cm-changedLine, .cm-inlineChangedLine":{backgroundColor:"rgba(100, 160, 128, .08)"},"&light.cm-merge-a .cm-changedText, &light .cm-deletedChunk .cm-deletedText":{background:"linear-gradient(#ee443366, #ee443366) bottom/100% 2px no-repeat"},"&dark.cm-merge-a .cm-changedText, &dark .cm-deletedChunk .cm-deletedText":{background:"linear-gradient(#ffaa9966, #ffaa9966) bottom/100% 2px no-repeat"},"&light.cm-merge-b .cm-changedText":{background:"linear-gradient(#22bb22aa, #22bb22aa) bottom/100% 2px no-repeat"},"&dark.cm-merge-b .cm-changedText":{background:"linear-gradient(#88ff88aa, #88ff88aa) bottom/100% 2px no-repeat"},"&.cm-merge-b .cm-deletedText":{background:"#ff000033"},".cm-insertedLine, .cm-deletedLine, .cm-deletedLine del":{textDecoration:"none"},".cm-deletedChunk":{paddingLeft:"6px","& .cm-chunkButtons":{position:"absolute",insetInlineEnd:"5px"},"& button":{border:"none",cursor:"pointer",color:"white",margin:"0 2px",borderRadius:"3px","&[name=accept]":{background:"#2a2"},"&[name=reject]":{background:"#d43"}}},".cm-collapsedLines":{padding:"5px 5px 5px 10px",cursor:"pointer","&:before":{content:'"⦚"',marginInlineEnd:"7px"},"&:after":{content:'"⦚"',marginInlineStart:"7px"}},"&light .cm-collapsedLines":{color:"#444",background:"linear-gradient(to bottom, transparent 0, #f3f3f3 30%, #f3f3f3 70%, transparent 100%)"},"&dark .cm-collapsedLines":{color:"#ddd",background:"linear-gradient(to bottom, transparent 0, #222 30%, #222 70%, transparent 100%)"},".cm-changeGutter":{width:"3px",paddingLeft:"1px"},"&light.cm-merge-a .cm-changedLineGutter, &light .cm-deletedLineGutter":{background:"#e43"},"&dark.cm-merge-a .cm-changedLineGutter, &dark .cm-deletedLineGutter":{background:"#fa9"},"&light.cm-merge-b .cm-changedLineGutter":{background:"#2b2"},"&dark.cm-merge-b .cm-changedLineGutter":{background:"#8f8"},".cm-inlineChangedLineGutter":{background:"#75d"}}),wt=new Mt,Ae=new Mt;class rr{constructor(e){this.revertDOM=null,this.revertToA=!1,this.revertToLeft=!1,this.measuring=-1,this.diffConf=e.diffConfig||Ut;let r=[_e.low(Se),Yt,nr,fe,T.updateListener.of(c=>{this.measuring<0&&(c.heightChanged||c.viewportChanged)&&!c.transactions.some(m=>m.effects.some(g=>g.is(Me)))&&this.measure()})],s=[V.of({side:"a",sibling:()=>this.b,highlightChanges:e.highlightChanges!==!1,markGutter:e.gutter!==!1})];e.gutter!==!1&&s.push(Ce);let i=Be.create({doc:e.a.doc,selection:e.a.selection,extensions:[e.a.extensions||[],T.editorAttributes.of({class:"cm-merge-a"}),Ae.of(s),r]}),t=[V.of({side:"b",sibling:()=>this.a,highlightChanges:e.highlightChanges!==!1,markGutter:e.gutter!==!1})];e.gutter!==!1&&t.push(Ce);let o=Be.create({doc:e.b.doc,selection:e.b.selection,extensions:[e.b.extensions||[],T.editorAttributes.of({class:"cm-merge-b"}),Ae.of(t),r]});this.chunks=q.build(i.doc,o.doc,this.diffConf);let a=[F.init(()=>this.chunks),wt.of(e.collapseUnchanged?Ge(e.collapseUnchanged):[])];i=i.update({effects:ne.appendConfig.of(a)}).state,o=o.update({effects:ne.appendConfig.of(a)}).state,this.dom=document.createElement("div"),this.dom.className="cm-mergeView",this.editorDOM=this.dom.appendChild(document.createElement("div")),this.editorDOM.className="cm-mergeViewEditors";let l=e.orientation||"a-b",u=document.createElement("div");u.className="cm-mergeViewEditor";let d=document.createElement("div");d.className="cm-mergeViewEditor",this.editorDOM.appendChild(l=="a-b"?u:d),this.editorDOM.appendChild(l=="a-b"?d:u),this.a=new T({state:i,parent:u,root:e.root,dispatchTransactions:c=>this.dispatch(c,this.a)}),this.b=new T({state:o,parent:d,root:e.root,dispatchTransactions:c=>this.dispatch(c,this.b)}),this.setupRevertControls(!!e.revertControls,e.revertControls=="b-to-a",e.renderRevertControl),e.parent&&e.parent.appendChild(this.dom),this.scheduleMeasure()}dispatch(e,r){if(e.some(s=>s.docChanged)){let s=e[e.length-1],i=e.reduce((o,a)=>o.compose(a.changes),_t.empty(e[0].startState.doc.length));this.chunks=r==this.a?q.updateA(this.chunks,s.newDoc,this.b.state.doc,i,this.diffConf):q.updateB(this.chunks,this.a.state.doc,s.newDoc,i,this.diffConf),r.update([...e,s.state.update({effects:Ne.of(this.chunks)})]);let t=r==this.a?this.b:this.a;t.update([t.state.update({effects:Ne.of(this.chunks)})]),this.scheduleMeasure()}else r.update(e)}reconfigure(e){if("diffConfig"in e&&(this.diffConf=e.diffConfig),"orientation"in e){let t=e.orientation!="b-a";if(t!=(this.editorDOM.firstChild==this.a.dom.parentNode)){let o=this.a.dom.parentNode,a=this.b.dom.parentNode;o.remove(),a.remove(),this.editorDOM.insertBefore(t?o:a,this.editorDOM.firstChild),this.editorDOM.appendChild(t?a:o),this.revertToLeft=!this.revertToLeft,this.revertDOM&&(this.revertDOM.textContent="")}}if("revertControls"in e||"renderRevertControl"in e){let t=!!this.revertDOM,o=this.revertToA,a=this.renderRevert;"revertControls"in e&&(t=!!e.revertControls,o=e.revertControls=="b-to-a"),"renderRevertControl"in e&&(a=e.renderRevertControl),this.setupRevertControls(t,o,a)}let r="highlightChanges"in e,s="gutter"in e,i="collapseUnchanged"in e;if(r||s||i){let t=[],o=[];if(r||s){let a=this.a.state.facet(V),l=s?e.gutter!==!1:a.markGutter,u=r?e.highlightChanges!==!1:a.highlightChanges;t.push(Ae.reconfigure([V.of({side:"a",sibling:()=>this.b,highlightChanges:u,markGutter:l}),l?Ce:[]])),o.push(Ae.reconfigure([V.of({side:"b",sibling:()=>this.a,highlightChanges:u,markGutter:l}),l?Ce:[]]))}if(i){let a=wt.reconfigure(e.collapseUnchanged?Ge(e.collapseUnchanged):[]);t.push(a),o.push(a)}this.a.dispatch({effects:t}),this.b.dispatch({effects:o})}this.scheduleMeasure()}setupRevertControls(e,r,s){this.revertToA=r,this.revertToLeft=this.revertToA==(this.editorDOM.firstChild==this.a.dom.parentNode),this.renderRevert=s,!e&&this.revertDOM?(this.revertDOM.remove(),this.revertDOM=null):e&&!this.revertDOM?(this.revertDOM=this.editorDOM.insertBefore(document.createElement("div"),this.editorDOM.firstChild.nextSibling),this.revertDOM.addEventListener("mousedown",i=>this.revertClicked(i)),this.revertDOM.className="cm-merge-revert"):this.revertDOM&&(this.revertDOM.textContent="")}scheduleMeasure(){if(this.measuring<0){let e=this.dom.ownerDocument.defaultView||window;this.measuring=e.requestAnimationFrame(()=>{this.measuring=-1,this.measure()})}}measure(){Xn(this.a,this.b,this.chunks),this.revertDOM&&this.updateRevertButtons()}updateRevertButtons(){let e=this.revertDOM,r=e.firstChild,s=this.a.viewport,i=this.b.viewport;for(let t=0;ts.to||o.fromB>i.to)break;if(o.fromA-1&&(this.dom.ownerDocument.defaultView||window).cancelAnimationFrame(this.measuring),this.dom.remove()}}function kt(n){let e=n.nextSibling;return n.remove(),e}const ir=new class extends $e{constructor(){super(...arguments),this.elementClass="cm-deletedLineGutter"}},or=_e.low(Rt({class:"cm-changeGutter",markers:n=>{var e;return((e=n.plugin(Se))===null||e===void 0?void 0:e.gutter)||Lt.empty},widgetMarker:(n,e)=>e instanceof zt?ir:null}));function sr(n){var e;let r=typeof n.original=="string"?Dt.of(n.original.split(/\r?\n/)):n.original,s=n.diffConfig||Ut;return[_e.low(Se),cr,Yt,T.editorAttributes.of({class:"cm-merge-b"}),Ht.of((i,t)=>{let o=t.effects.find(a=>a.is(We));return o&&(i=q.updateA(i,o.value.doc,t.startState.doc,o.value.changes,s)),t.docChanged&&(i=q.updateB(i,t.state.field(oe),t.newDoc,t.changes,s)),i}),V.of({highlightChanges:n.highlightChanges!==!1,markGutter:n.gutter!==!1,syntaxHighlightDeletions:n.syntaxHighlightDeletions!==!1,syntaxHighlightDeletionsMaxLength:3e3,mergeControls:(e=n.mergeControls)!==null&&e!==void 0?e:!0,overrideChunk:n.allowInlineDiffs?mr:void 0,side:"b"}),oe.init(()=>r),n.gutter!==!1?or:[],n.collapseUnchanged?Ge(n.collapseUnchanged):[],F.init(i=>q.build(r,i.doc,s))]}const We=ne.define(),oe=he.define({create:()=>Dt.empty,update(n,e){for(let r of e.effects)r.is(We)&&(n=r.value.doc);return n}}),Ct=new WeakMap;class zt extends De{constructor(e){super(),this.buildDOM=e,this.dom=null}eq(e){return this.dom==e.dom}toDOM(e){return this.dom||(this.dom=this.buildDOM(e))}}function ar(n,e,r){let s=Ct.get(e.changes);if(s)return s;let i=o=>{let{highlightChanges:a,syntaxHighlightDeletions:l,syntaxHighlightDeletionsMaxLength:u,mergeControls:d}=n.facet(V),c=document.createElement("div");if(c.className="cm-deletedChunk",d){let _=c.appendChild(document.createElement("div"));_.className="cm-chunkButtons";let S=A=>{A.preventDefault(),lr(o,o.posAtDOM(c))},B=A=>{A.preventDefault(),dr(o,o.posAtDOM(c))};if(typeof d=="function")_.appendChild(d("accept",S)),_.appendChild(d("reject",B));else{let A=_.appendChild(document.createElement("button"));A.name="accept",A.textContent=n.phrase("Accept"),A.onmousedown=S;let R=_.appendChild(document.createElement("button"));R.name="reject",R.textContent=n.phrase("Reject"),R.onmousedown=B}}if(r||e.fromA>=e.toA)return c;let m=o.state.field(oe).sliceString(e.fromA,e.endA),g=l&&n.facet(xn),p=D(),x=e.changes,v=0,y=!1;function D(){let _=c.appendChild(document.createElement("div"));return _.className="cm-deletedLine",_.appendChild(document.createElement("del"))}function G(_,S,B){for(let A=_;A-1&&YA){let H=document.createTextNode(m.slice(A,R));if(me){let pe=p.appendChild(document.createElement("span"));pe.className=me,pe.appendChild(H)}else p.appendChild(H);A=R}ge&&(y=!y)}}if(g&&e.toA-e.fromA<=u){let _=g.parser.parse(m),S=0;wn(_,{style:B=>kn(n,B)},(B,A,R)=>{B>S&&G(S,B,""),G(B,A,R),S=A}),G(S,m.length,"")}else G(0,m.length,"");return p.firstChild||p.appendChild(document.createElement("br")),c},t=E.widget({block:!0,side:-1,widget:new zt(i)});return Ct.set(e.changes,t),t}function lr(n,e){let{state:r}=n,s=e??r.selection.main.head,i=n.state.field(F).find(l=>l.fromB<=s&&l.endB>=s);if(!i)return!1;let t=n.state.sliceDoc(i.fromB,Math.max(i.fromB,i.toB-1)),o=n.state.field(oe);i.fromB!=i.toB&&i.toA<=o.length&&(t+=n.state.lineBreak);let a=_t.of({from:i.fromA,to:Math.min(o.length,i.toA),insert:t},o.length);return n.dispatch({effects:We.of({doc:a.apply(o),changes:a}),userEvent:"accept"}),!0}function dr(n,e){let{state:r}=n,s=e??r.selection.main.head,i=r.field(F).find(a=>a.fromB<=s&&a.endB>=s);if(!i)return!1;let o=r.field(oe).sliceString(i.fromA,Math.max(i.fromA,i.toA-1));return i.fromA!=i.toA&&i.toB<=r.doc.length&&(o+=r.lineBreak),n.dispatch({changes:{from:i.fromB,to:Math.min(r.doc.length,i.toB),insert:o},userEvent:"revert"}),!0}function yt(n){let e=new re;for(let r of n.field(F)){let s=n.facet(V).overrideChunk&&Xt(n,r);e.add(r.fromB,r.fromB,ar(n,r,!!s))}return e.finish()}const cr=he.define({create:n=>yt(n),update(n,e){return e.state.field(F,!1)!=e.startState.field(F,!1)?yt(e.state):n},provide:n=>T.decorations.from(n)}),Ot=new WeakMap;function Xt(n,e){let r=Ot.get(e);if(r!==void 0)return r;r=null;let s=n.field(oe),i=n.doc,t=s.lineAt(e.endA).number-s.lineAt(e.fromA).number+1,o=i.lineAt(e.endB).number-i.lineAt(e.fromB).number+1;e:if(t==o&&t<10){let a=[],l=0,u=e.fromA,d=e.fromB;for(let c of e.changes){if(c.fromA=e.endB)break;o=n.doc.lineAt(o.to+1)}return!0}const gr=Cn({String:Z.string,Number:Z.number,"True False":Z.bool,PropertyName:Z.propertyName,Null:Z.null,", :":Z.separator,"[ ]":Z.squareBracket,"{ }":Z.brace}),pr=yn.deserialize({version:14,states:"$bOVQPOOOOQO'#Cb'#CbOnQPO'#CeOvQPO'#ClOOQO'#Cr'#CrQOQPOOOOQO'#Cg'#CgO}QPO'#CfO!SQPO'#CtOOQO,59P,59PO![QPO,59PO!aQPO'#CuOOQO,59W,59WO!iQPO,59WOVQPO,59QOqQPO'#CmO!nQPO,59`OOQO1G.k1G.kOVQPO'#CnO!vQPO,59aOOQO1G.r1G.rOOQO1G.l1G.lOOQO,59X,59XOOQO-E6k-E6kOOQO,59Y,59YOOQO-E6l-E6l",stateData:"#O~OeOS~OQSORSOSSOTSOWQO_ROgPO~OVXOgUO~O^[O~PVO[^O~O]_OVhX~OVaO~O]bO^iX~O^dO~O]_OVha~O]bO^ia~O",goto:"!kjPPPPPPkPPkqwPPPPk{!RPPP!XP!e!hXSOR^bQWQRf_TVQ_Q`WRg`QcZRicQTOQZRQe^RhbRYQR]R",nodeNames:"⚠ JsonText True False Null Number String } { Object Property PropertyName : , ] [ Array",maxTerm:25,nodeProps:[["isolate",-2,6,11,""],["openedBy",7,"{",14,"["],["closedBy",8,"}",15,"]"]],propSources:[gr],skippedNodes:[0],repeatNodeCount:2,tokenData:"(|~RaXY!WYZ!W]^!Wpq!Wrs!]|}$u}!O$z!Q!R%T!R![&c![!]&t!}#O&y#P#Q'O#Y#Z'T#b#c'r#h#i(Z#o#p(r#q#r(w~!]Oe~~!`Wpq!]qr!]rs!xs#O!]#O#P!}#P;'S!];'S;=`$o<%lO!]~!}Og~~#QXrs!]!P!Q!]#O#P!]#U#V!]#Y#Z!]#b#c!]#f#g!]#h#i!]#i#j#m~#pR!Q![#y!c!i#y#T#Z#y~#|R!Q![$V!c!i$V#T#Z$V~$YR!Q![$c!c!i$c#T#Z$c~$fR!Q![!]!c!i!]#T#Z!]~$rP;=`<%l!]~$zO]~~$}Q!Q!R%T!R![&c~%YRT~!O!P%c!g!h%w#X#Y%w~%fP!Q![%i~%nRT~!Q![%i!g!h%w#X#Y%w~%zR{|&T}!O&T!Q![&Z~&WP!Q![&Z~&`PT~!Q![&Z~&hST~!O!P%c!Q![&c!g!h%w#X#Y%w~&yO[~~'OO_~~'TO^~~'WP#T#U'Z~'^P#`#a'a~'dP#g#h'g~'jP#X#Y'm~'rOR~~'uP#i#j'x~'{P#`#a(O~(RP#`#a(U~(ZOS~~(^P#f#g(a~(dP#i#j(g~(jP#X#Y(m~(rOQ~~(wOW~~(|OV~",tokenizers:[0],topRules:{JsonText:[0,1]},tokenPrec:0}),vr=An.define({name:"json",parser:pr.configure({props:[Bn.add({Object:it({except:/^\s*\}/}),Array:it({except:/^\s*\]/})}),Mn.add({"Object Array":_n})]}),languageData:{closeBrackets:{brackets:["[","{",'"']},indentOnInput:/^\s*[\}\]]$/}});function br(){return new On(vr)}const xr={class:"space-y-6"},wr={class:"flex items-end justify-between gap-4 flex-wrap"},kr={class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"},Cr={class:"flex items-center gap-2"},yr={class:"pb-5 border-b border-border"},Or={class:"grid gap-3 sm:gap-4 sm:grid-cols-[1fr_auto_1fr] items-end"},Ar={class:"space-y-1.5"},Br=["value"],Mr=["value","disabled"],_r=["disabled"],Dr={class:"space-y-1.5"},Sr=["value"],Rr=["value","disabled"],Lr={key:0,class:"mt-4 flex flex-wrap items-center gap-2 justify-end"},Pr={key:0,class:"text-sm text-foreground-muted py-12 text-center"},Tr={key:1,class:"text-sm text-foreground-muted py-12 text-center"},Er={key:2,class:"bg-warning/10 border border-warning/30 rounded-lg p-5 space-y-3 text-sm"},Qr={key:0,class:"font-mono text-xs space-y-1"},Nr=["title","onClick"],Vr={key:1,class:"text-warning/90"},Gr={key:1,class:"text-warning/70 italic text-xs"},$r={key:3,class:"bg-danger/10 border border-danger/30 rounded-lg p-5 text-sm text-danger"},Fr={key:4,class:"bg-danger/10 border border-danger/30 rounded-lg p-5 text-sm text-danger"},jr={key:0,class:"text-xs text-foreground-muted -mt-2"},Ir={key:1,class:"bg-background border border-border rounded-lg"},Wr={class:"px-4 py-3 flex items-center justify-between gap-3"},Hr={class:"text-xs font-bold uppercase tracking-wider text-foreground-muted flex items-center gap-2"},qr={class:"text-[10px] px-1.5 py-0.5 rounded bg-warning/15 text-warning border border-warning/30 font-medium tracking-normal normal-case"},Ur=["aria-label"],Zr={key:0,class:"px-4 pb-4 pt-3 border-t border-border/60"},Yr={class:"font-mono text-xs space-y-1"},zr={key:2,class:"bg-background border border-border rounded-lg overflow-hidden"},Xr={class:"px-4 py-2.5 flex items-center justify-between gap-3 bg-surface border-b border-border"},Jr={class:"text-xs font-bold uppercase tracking-wider text-white flex items-center gap-2"},Kr={class:"font-mono normal-case tracking-normal text-sm font-medium"},ei={key:0,class:"text-[10px] px-1.5 py-0.5 rounded bg-success/15 text-success border border-success/30 font-medium tracking-normal normal-case"},ti={key:1,class:"text-[10px] px-1.5 py-0.5 rounded bg-danger/15 text-danger border border-danger/30 font-medium tracking-normal normal-case"},ni=["title","aria-pressed"],ri={key:3,class:"bg-background border border-border rounded-lg overflow-hidden"},ii={class:"px-4 py-2.5 flex items-center justify-between gap-3 bg-surface border-b border-border"},oi={class:"text-xs font-bold uppercase tracking-wider text-foreground-muted flex items-center gap-2"},si={class:"font-mono normal-case tracking-normal text-white text-sm font-medium"},ai=["aria-label"],li={key:4,class:"text-sm text-foreground-muted py-12 text-center"},At="orva:diff:sideBySide",bi={__name:"FunctionDiff",setup(n){const e=hn(),r=fn(),s=$(()=>e.params.name),i=$(()=>e.query.from||""),t=$(()=>e.query.to||""),o=P(null),a=P([]),l=P(null),u=P(""),d=P(""),c=P([]),m=P("Copy link"),g=P(!0),p=P(!0),x=P(!1),v=sn(),y=P(!0),D=P(null),G=$(()=>D.value===null?y.value:D.value);try{const h=typeof window<"u"&&window.localStorage?.getItem?.(At);h==="true"?D.value=!0:h==="false"&&(D.value=!1)}catch{}const _=()=>{const h=!G.value;D.value=h;try{window.localStorage?.setItem?.(At,String(h))}catch{}ve()},S=P(null),B=P(null);let A=null,R=null;const me=$(()=>o.value?.runtime?.startsWith("python")?[Rn()]:[Ln()]),ge=T.theme({"&.cm-merge-a .cm-changedLine":{backgroundColor:"rgba(248, 81, 73, 0.15)"},"&.cm-merge-b .cm-changedLine":{backgroundColor:"rgba(63, 185, 80, 0.15)"},"&.cm-merge-a .cm-changedText":{background:"none",backgroundColor:"rgba(248, 81, 73, 0.35)",borderRadius:"2px"},"&.cm-merge-b .cm-changedText":{background:"none",backgroundColor:"rgba(63, 185, 80, 0.30)",borderRadius:"2px"},"&.cm-merge-a .cm-changedLineGutter":{color:"#f85149"},"&.cm-merge-b .cm-changedLineGutter":{color:"#3fb950"},".cm-deletedChunk":{backgroundColor:"rgba(248, 81, 73, 0.15)"},".cm-deletedChunk .cm-deletedText":{background:"none",backgroundColor:"rgba(248, 81, 73, 0.35)",borderRadius:"2px"},".cm-deletedChunk .cm-changedLineGutter, .cm-deletedChunk .cm-deletedLineGutter":{color:"#f85149"},".cm-insertedLine, .cm-changedLine, .cm-inlineChangedLine":{backgroundColor:"rgba(63, 185, 80, 0.15)"},".cm-changedLine .cm-changedText, .cm-inlineChangedLine .cm-changedText":{background:"none",backgroundColor:"rgba(63, 185, 80, 0.30)",borderRadius:"2px"},".cm-changedLineGutter, .cm-inlineChangedLineGutter":{color:"#3fb950"}}),Y=h=>[Dn,...h,Sn,ge,Be.readOnly.of(!0),T.lineWrapping];let H=null;const pe=()=>{if(typeof window>"u"||!window.matchMedia)return;const h=window.matchMedia("(min-width: 768px)"),f=O=>{const b=!!O.matches;b!==y.value&&(y.value=b,D.value===null&&ve())};f(h),h.addEventListener?(h.addEventListener("change",f),H=()=>h.removeEventListener("change",f)):(h.addListener(f),H=()=>h.removeListener(f))},He=(h,f,O,b)=>h?G.value?new rr({a:{doc:f,extensions:Y(b)},b:{doc:O,extensions:Y(b)},parent:h,orientation:"a-b",revertControls:!1,highlightChanges:!0,gutter:!0}):new T({parent:h,state:Be.create({doc:O,extensions:[...Y(b),sr({original:f,mergeControls:!1})]})}):null,qe=()=>{R?.destroy?.(),R=null},Re=()=>{A?.destroy?.(),qe(),A=null},Ue=()=>{const h=U.value;!h||!p.value||h.before===h.after&&!h.added&&!h.removed||(R=He(B.value,h.before||"",h.after||"",[br()]))},ve=async()=>{if(await nt(),Re(),!l.value)return;const h=K.value;h&&(A=He(S.value,h.before||"",h.after||"",me.value)),Ue()},Ze=async()=>{if(u.value="",d.value="",l.value=null,Re(),!(!o.value||!i.value||!t.value||i.value===t.value))try{const h=await cn(o.value.id,i.value,t.value,"json");l.value=h.data,await ve()}catch(h){const f=h.response?.data?.error;u.value=f?.code||"INTERNAL",d.value=f?.message||h.message||"Failed to load diff.",u.value==="VERSION_GCD"&&(c.value=f?.details?.available_hashes||[])}},K=$(()=>l.value?.files?.find(h=>h.kind==="handler")),U=$(()=>l.value?.files?.find(h=>h.kind==="manifest")),be=$(()=>{const h=l.value?.from?.snapshot,f=l.value?.to?.snapshot;return!h||!f?[]:rt(h,f)}),Jt=h=>h.startsWith("+")?"text-success":h.startsWith("-")?"text-danger":"text-warning",Ye=$(()=>{const h=o.value?.code_hash||"";return a.value.filter(f=>f.status==="succeeded"&&f.code_hash).map(f=>({id:f.id,version:f.version,shortHash:(f.code_hash||"").slice(0,12),submittedAt:f.submitted_at,isActive:!!h&&f.code_hash===h}))}),ze=h=>{if(!h)return"";const f=new Date(h),O=f.toLocaleDateString(void 0,{month:"numeric",day:"numeric"}),b=f.toLocaleTimeString(void 0,{hour:"numeric",minute:"2-digit"});return`${O} ${b}`},Xe=h=>`v${h.version} · ${h.shortHash} · ${ze(h.submittedAt)}${h.isActive?" · active":""}`,ee=({from:h,to:f})=>{r.replace({query:{from:h??i.value,to:f??t.value}})},Kt=()=>{!i.value||!t.value||r.replace({query:{from:t.value,to:i.value}})},en=async()=>{const h=await pn(window.location.href);m.value=h?"Copied!":"Copy failed",setTimeout(()=>{m.value="Copy link"},1500)},se=$(()=>{const h=o.value?.code_hash;return h&&a.value.find(O=>O.status==="succeeded"&&O.code_hash===h)?.id||null}),te=$(()=>a.value.find(h=>h.id===i.value)||null),Je=$(()=>!!(te.value&&o.value?.code_hash&&te.value.code_hash===o.value.code_hash)),tn=()=>{if(!se.value||!o.value)return;const h=o.value.code_hash,O=a.value.filter(b=>b.status==="succeeded"&&b.code_hash&&b.code_hash!==h)[0];ee({from:O?.id||i.value,to:se.value})},ae=h=>a.value.find(f=>f.code_hash===h)||null,nn=h=>{const f=ae(h);if(!f)return;const O=a.value.find(b=>b.id===t.value);O&&c.value.length&&!c.value.includes(O.code_hash)?ee({from:f.id,to:se.value||t.value}):ee({from:f.id,to:t.value})},rn=async()=>{try{const f=((await Ke()).data?.functions||[]).find(b=>b.name===s.value);f&&(o.value=f);const O=await et(o.value.id,100);a.value=O.data.deployments||O.data||[]}catch{}},on=async()=>{if(x.value||!o.value||!te.value||Je.value)return;const h=te.value,f=(h.code_hash||"").slice(0,12);let O=`Code hash ${f}. The current version stays in history.`;try{const z=(await mn(h.id))?.data?.snapshot;if(z){const le=rt(o.value,z);le.length?O=`Rolling back to v${h.version} (code ${f}) will also change: - -${le.join(` -`)} - -Secrets keep their current values; they aren't part of the rollback.`:O=`Rolling back to v${h.version} (code ${f}). Settings and env are already identical, so only the code changes.`}}catch{}if(await v.ask({title:`Restore v${h.version}?`,message:O,confirmLabel:"Rollback"})){x.value=!0;try{const j=await gn(o.value.id,{deployment_id:h.id}),z=j?.data?.id||j?.data?.deployment_id;await rn(),z&&ee({from:i.value,to:z}),v.notify({title:"Rollback complete",message:`v${h.version} (code ${f}) is now serving.`})}catch(j){const z=j.response?.data?.error?.code||"",le=j.response?.data?.error?.message||j.message||"Rollback failed";z==="VERSION_GCD"?v.notify({title:"Version unavailable",message:`This version has been garbage-collected and can no longer be restored. - -${le}`,danger:!0}):v.notify({title:"Rollback failed",message:le,danger:!0})}finally{x.value=!1}}};return an(async()=>{pe();try{const f=((await Ke()).data?.functions||[]).find(O=>O.name===s.value);if(!f)throw new Error("not found");o.value=f}catch(h){u.value=h.response?.data?.error?.code||"FUNCTION_NOT_FOUND",d.value=h.response?.data?.error?.message||"Function not found.";return}try{const h=await et(o.value.id,100);a.value=h.data.deployments||h.data||[]}catch{a.value=[]}await Ze()}),Le(p,async h=>{await nt(),qe(),h&&Ue()}),Le([i,t],Ze),Le(()=>o.value?.runtime,()=>{ve()}),ln(()=>{Re(),H?.()}),(h,f)=>{const O=un("router-link");return k(),C("div",xr,[w("div",wr,[w("div",null,[f[7]||(f[7]=w("h1",{class:"text-xl font-semibold text-white tracking-tight text-balance"}," Compare versions ",-1)),w("p",kr,[f[5]||(f[5]=Q(" Source diff between two past deployments of ",-1)),N(O,{to:`/functions/${s.value}`,class:"text-white underline"},{default:xe(()=>[Q(L(s.value),1)]),_:1},8,["to"]),f[6]||(f[6]=Q(". ",-1))])]),w("div",Cr,[w("button",{class:"text-xs text-foreground-muted hover:text-white hover:bg-surface-hover rounded-md flex items-center justify-center gap-1.5 px-2 py-1.5 transition-colors min-w-[6rem] focus:outline-none focus-visible:ring-1 focus-visible:ring-white",title:"Copy share link","aria-label":"Copy share link",onClick:en},[N(I(Pn),{class:"w-3.5 h-3.5"}),Q(" "+L(m.value),1)]),N(tt,{variant:"secondary",onClick:f[0]||(f[0]=b=>h.$router.push(`/functions/${s.value}/deployments`))},{default:xe(()=>[N(I($n),{class:"w-4 h-4 mr-2"}),f[8]||(f[8]=Q(" All deployments ",-1))]),_:1})])]),w("div",yr,[w("div",Or,[w("div",Ar,[f[10]||(f[10]=w("label",{class:"block text-[10px] font-bold uppercase tracking-label text-foreground-muted"},[Q(" From "),w("span",{class:"ml-1 text-foreground-muted/60 normal-case font-medium tracking-normal"},"(older)")],-1)),w("select",{value:i.value,class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm font-mono text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onChange:f[1]||(f[1]=b=>ee({from:b.target.value}))},[f[9]||(f[9]=w("option",{value:"",disabled:""}," Pick a version ",-1)),(k(!0),C(de,null,we(Ye.value,b=>(k(),C("option",{key:b.id,value:b.id,disabled:b.id===t.value},L(Xe(b)),9,Mr))),128))],40,Br)]),w("button",{disabled:!i.value||!t.value,class:"justify-self-center sm:self-end mb-1 h-9 w-9 flex items-center justify-center rounded-md text-foreground-muted hover:text-white hover:bg-surface-hover transition-colors disabled:opacity-30 disabled:hover:bg-transparent disabled:hover:text-foreground-muted",title:"Swap from / to","aria-label":"Swap from and to versions",onClick:Kt},[N(I(Gn),{class:"w-4 h-4 rotate-90 sm:rotate-0 motion-reduce:transition-none transition-transform"})],8,_r),w("div",Dr,[f[12]||(f[12]=w("label",{class:"block text-[10px] font-bold uppercase tracking-label text-foreground-muted"},[Q(" To "),w("span",{class:"ml-1 text-foreground-muted/60 normal-case font-medium tracking-normal"},"(newer)")],-1)),w("select",{value:t.value,class:"w-full bg-background border border-border rounded-md px-3 py-2 text-sm font-mono text-foreground focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onChange:f[2]||(f[2]=b=>ee({to:b.target.value}))},[f[11]||(f[11]=w("option",{value:"",disabled:""}," Pick a version ",-1)),(k(!0),C(de,null,we(Ye.value,b=>(k(),C("option",{key:b.id,value:b.id,disabled:b.id===i.value},L(Xe(b)),9,Rr))),128))],40,Sr)])]),o.value?(k(),C("div",Lr,[se.value&&t.value!==se.value?(k(),C("button",{key:0,class:"inline-flex items-center gap-1.5 text-xs text-foreground-muted hover:text-white hover:bg-surface-hover rounded-md px-2.5 py-1.5 transition-colors",title:"Compare previous version with currently active",onClick:tn},[N(I(Tn),{class:"w-3.5 h-3.5"}),f[13]||(f[13]=Q(" vs active ",-1))])):W("",!0),te.value&&!Je.value?(k(),dn(tt,{key:1,variant:"primary",size:"sm",loading:x.value,disabled:x.value,onClick:on},{default:xe(()=>[N(I(Vn),{class:"w-3.5 h-3.5 mr-1.5"}),Q(" Roll back to v"+L(te.value.version),1)]),_:1},8,["loading","disabled"])):W("",!0)])):W("",!0)]),!i.value||!t.value?(k(),C("p",Pr," Pick two different deployments above to compare. ")):i.value===t.value?(k(),C("p",Tr," Same version on both sides. Pick a different deployment to see a diff. ")):u.value==="VERSION_GCD"?(k(),C("div",Er,[f[14]||(f[14]=w("div",{class:"text-warning font-medium"}," Source data for one or both versions has been garbage-collected. ",-1)),f[15]||(f[15]=w("p",{class:"text-warning/80 leading-body"}," The deployment row still exists, but the on-disk code tree was pruned by the version GC. Pick a version whose source is still archived: ",-1)),c.value.length?(k(),C("ul",Qr,[(k(!0),C(de,null,we(c.value,b=>(k(),C("li",{key:b},[ae(b)?(k(),C("button",{key:0,class:"text-left text-warning hover:text-white hover:bg-warning/15 rounded px-1.5 py-0.5 -mx-1.5 transition-colors",title:`Use v${ae(b).version} as the From side`,onClick:j=>nn(b)}," v"+L(ae(b).version)+" · "+L(b.slice(0,12))+"… · "+L(ze(ae(b).submitted_at)),9,Nr)):(k(),C("span",Vr,L(b.slice(0,12))+"…",1))]))),128))])):(k(),C("p",Gr," No surviving on-disk versions for this function. Redeploy the original source to compare against the current code. "))])):u.value==="VERSION_NOT_FOUND"?(k(),C("div",$r,[f[17]||(f[17]=Q(" One of the supplied deployment IDs doesn't exist. Pick from the dropdowns above, or go back to the ",-1)),N(O,{to:`/functions/${s.value}/deployments`,class:"underline"},{default:xe(()=>[...f[16]||(f[16]=[Q(" deployment history ",-1)])]),_:1},8,["to"]),f[18]||(f[18]=Q(". ",-1))])):u.value&&u.value!=="OK"?(k(),C("div",Fr,L(d.value||"Failed to load diff."),1)):l.value?(k(),C(de,{key:5},[be.value.length?(k(),C("section",Ir,[w("header",Wr,[w("h2",Hr,[N(I(En),{class:"w-3.5 h-3.5"}),f[19]||(f[19]=Q(" Settings & env ",-1)),w("span",qr,L(be.value.length)+" change"+L(be.value.length===1?"":"s"),1)]),w("button",{class:"text-foreground-muted hover:text-white","aria-label":g.value?"Collapse settings diff":"Expand settings diff",onClick:f[3]||(f[3]=b=>g.value=!g.value)},[N(I(ot),{class:Pe(["w-4 h-4 transition-transform motion-reduce:transition-none",{"rotate-180":g.value}])},null,8,["class"])],8,Ur)]),g.value?(k(),C("div",Zr,[w("ul",Yr,[(k(!0),C(de,null,we(be.value,(b,j)=>(k(),C("li",{key:j,class:Pe(Jt(b))},L(b),3))),128))]),f[20]||(f[20]=w("p",{class:"text-[11px] text-foreground-muted/70 mt-3"}," Secrets aren't tracked per-version; they always reflect the current values. ",-1))])):W("",!0)])):(k(),C("p",jr," Settings and env are identical between these versions. Secrets aren't tracked per-version. ")),K.value?(k(),C("section",zr,[w("header",Xr,[w("h2",Jr,[N(I(Qn),{class:"w-3.5 h-3.5 text-foreground-muted"}),w("span",Kr,L(K.value.path),1),K.value.added?(k(),C("span",ei,"added")):K.value.removed?(k(),C("span",ti,"removed")):W("",!0)]),w("button",{class:"text-[10px] font-medium uppercase tracking-label text-foreground-muted hover:text-white hover:bg-surface-hover rounded px-2 py-1 transition-colors",title:G.value?"Switch to unified inline view":"Switch to side-by-side view","aria-pressed":G.value,onClick:_},L(G.value?"side-by-side":"unified"),9,ni)]),w("div",{ref_key:"codeMountRef",ref:S,class:"orva-merge"},null,512)])):W("",!0),U.value&&(U.value.before!==U.value.after||U.value.added||U.value.removed)?(k(),C("section",ri,[w("header",ii,[w("h2",oi,[N(I(Nn),{class:"w-3.5 h-3.5"}),w("span",si,L(U.value.path),1),f[21]||(f[21]=w("span",{class:"text-[10px] px-1.5 py-0.5 rounded bg-warning/15 text-warning border border-warning/30 font-medium tracking-normal normal-case"}," changed ",-1))]),w("button",{class:"text-foreground-muted hover:text-white","aria-label":p.value?"Collapse manifest diff":"Expand manifest diff",onClick:f[4]||(f[4]=b=>p.value=!p.value)},[N(I(ot),{class:Pe(["w-4 h-4 transition-transform motion-reduce:transition-none",{"rotate-180":p.value}])},null,8,["class"])],8,ai)]),p.value?(k(),C("div",{key:0,ref_key:"manifestMountRef",ref:B,class:"orva-merge"},null,512)):W("",!0)])):W("",!0),!K.value&&!U.value?(k(),C("p",li," No code changes between these versions. ")):W("",!0)],64)):W("",!0)])}}};export{bi as default}; diff --git a/backend/internal/server/ui_dist/assets/FunctionsList-Ca9x79GA.js b/backend/internal/server/ui_dist/assets/FunctionsList-Ca9x79GA.js deleted file mode 100644 index f17e976..0000000 --- a/backend/internal/server/ui_dist/assets/FunctionsList-Ca9x79GA.js +++ /dev/null @@ -1,3 +0,0 @@ -import{c as ee,C as te,D as se,o as oe,y as ne,E as le,G as ae,a as r,b as t,k as c,d,h as L,f as l,_ as F,S as ie,e as re,v as de,t as n,F as j,p as A,g as u,n as ue,T as ce,H as me,i as pe,r as g,q as S,j as i,P as B,s as xe,I as ve,J as G}from"./index-WXhXpu06.js";import{_ as w}from"./IconButton-CclydhPm.js";import{c as fe}from"./clipboard-CmSw2rR-.js";import{R as he}from"./refresh-cw-CEunRyai.js";import{G as V}from"./globe-DI_Jf14z.js";import{L as q}from"./lock-CDky0HD2.js";import{T as P}from"./trash-2-dFLn8UYZ.js";import{C as ge}from"./check-CuO1EVch.js";import{C as be}from"./copy-HWkx-vox.js";const O=ee("pencil",[["path",{d:"M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z",key:"1a8usu"}],["path",{d:"m15 5 4 4",key:"1mk7zo"}]]),ye={class:"space-y-6"},_e={class:"flex items-start justify-between gap-4 flex-wrap"},we={key:0,class:"space-y-3"},ke={class:"flex items-center gap-2 flex-wrap"},Ce={class:"relative flex-1 min-w-0 sm:min-w-[280px] max-w-full sm:max-w-[440px]"},$e={class:"text-[11px] text-foreground-muted shrink-0 tabular-nums"},De={class:"bg-background border border-border rounded-lg overflow-x-auto"},Le={class:"sm:hidden divide-y divide-border"},Se={class:"flex items-start justify-between gap-2"},Te={class:"min-w-0 flex-1"},Ee={class:"flex items-center gap-1.5 flex-wrap"},Re={class:"font-medium text-white truncate"},Fe={key:0,class:"inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-[10px] bg-warning-tint text-warning-fg border border-warning-ring"},Pe={key:1,class:"inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-[10px] bg-info-tint text-info-fg border border-info-ring"},ze={key:0,class:"mt-1 text-xs text-foreground-muted line-clamp-2"},Ne={class:"mt-1.5 flex items-center gap-3 text-[11px] text-foreground-muted font-mono"},Ue={class:"flex items-center gap-1 shrink-0"},Ie={key:0,class:"px-6 py-8 text-center text-sm text-foreground-muted space-y-3"},Me={class:"hidden sm:table w-full text-sm text-left"},je={class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},Ae={class:"px-4 py-3 w-8"},Be=["checked",".indeterminate"],Ge={class:"divide-y divide-border"},Ve={class:"px-4 py-3 align-middle"},qe=["checked","onChange"],Oe={class:"px-4 py-3 font-medium text-white"},He={class:"flex items-center gap-2 flex-wrap"},Je={key:0,class:"inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-[10px] bg-warning-tint text-warning-fg border border-warning-ring",title:"Outbound network enabled"},Ze=["title"],Ke=["title"],Qe=["title"],We={class:"px-4 py-3 text-foreground hidden sm:table-cell"},Xe={class:"inline-flex items-center px-2 py-0.5 rounded text-xs border border-border bg-background text-foreground-muted font-mono"},Ye={class:"px-4 py-3 text-foreground-muted font-mono text-xs hidden lg:table-cell"},et={class:"px-4 py-3 hidden md:table-cell align-middle"},tt={class:"flex items-center gap-2 min-w-0"},st=["title"],ot={class:"px-4 py-3 text-foreground-muted hidden xl:table-cell"},nt={class:"px-4 py-3 text-right"},lt={class:"inline-flex items-center gap-1"},at={key:0},it={colspan:"7",class:"px-6 py-8 text-center text-foreground-muted"},rt={class:"space-y-3"},dt={key:0,class:"flex justify-center border-t border-border py-3 bg-surface/30"},ut=["disabled"],ct={key:1,class:"bg-background border border-border rounded-lg p-8 text-center space-y-4"},mt={key:0,class:"fixed bottom-[calc(1rem+env(safe-area-inset-bottom,0px))] left-1/2 -translate-x-1/2 z-30 flex items-center gap-3 bg-background border border-border shadow-2xl rounded-full pl-4 pr-2 py-2"},pt={class:"text-xs text-white"},xt=25,$t={__name:"FunctionsList",setup(vt){const y=te(),k=pe(),m=g([]),T=g(0),x=g(!1),v=g(""),b=g(""),C=g(""),E=g(!1),a=g(new Set),f=S(()=>{const o=v.value.trim().toLowerCase();return o?m.value.filter(s=>s.name?.toLowerCase().includes(o)||s.description?.toLowerCase().includes(o)||s.runtime?.toLowerCase().includes(o)||s.id?.toLowerCase().includes(o)):m.value}),H=S(()=>m.value.lengthf.value.length>0&&f.value.every(o=>a.value.has(o.id))),J=S(()=>f.value.some(o=>a.value.has(o.id))),Z=o=>{const s=new Set(a.value);s.has(o)?s.delete(o):s.add(o),a.value=s},K=()=>{if(R.value)a.value=new Set;else{const o=new Set(a.value);f.value.forEach(s=>o.add(s.id)),a.value=o}},z=o=>`${window.location.origin}/fn/${o.id}`,Q=async o=>{await fe(z(o))?(b.value=o.id,setTimeout(()=>{b.value===o.id&&(b.value="")},1500)):y.notify({title:"Copy failed",message:`Could not copy to clipboard. URL: - -`+z(o)})},_=async o=>{x.value=!0;try{const s=await me({limit:xt,offset:o}),e=s.data.functions||[];T.value=s.data.total??e.length,o===0?m.value=e:m.value=[...m.value,...e]}catch(s){console.error(s)}finally{x.value=!1}},W=()=>_(m.value.length),N=()=>_(0),U=async o=>{if(await y.ask({title:`Delete "${o.name}"?`,message:"This is irreversible. Code, deployments, secrets, and routes for this function are removed.",confirmLabel:"Delete",danger:!0})){C.value=o.id;try{await G.delete(`/functions/${o.id}`),await N(),a.value.delete(o.id),a.value=new Set(a.value)}catch(e){const p=e.response?.data?.error?.message||e.message||"Delete failed";y.notify({title:"Delete failed",message:p,danger:!0})}finally{C.value=""}}},X=async()=>{const o=a.value.size;if(!await y.ask({title:`Delete ${o} ${o===1?"function":"functions"}?`,message:"Each one is irreversible. Code, deployments, secrets, and routes are removed.",confirmLabel:`Delete ${o}`,danger:!0}))return;E.value=!0;const e=[...a.value];let p=0;try{for(const Y of e)try{await G.delete(`/functions/${Y}`)}catch{p++}a.value=new Set,await N(),p&&y.notify({title:"Some deletes failed",message:`${p} of ${e.length} could not be deleted.`,danger:!0})}finally{E.value=!1}},I=se();let h=null;const M=()=>{h||(h=setTimeout(()=>{h=null,_(0)},300))};let $=null,D=null;return oe(()=>{_(0),$=I.subscribe("function",M),D=I.subscribe("deployment",M)}),ne(()=>{$&&($(),$=null),D&&(D(),D=null),h&&(clearTimeout(h),h=null)}),le(()=>_(0)),ae(()=>{h&&(clearTimeout(h),h=null)}),(o,s)=>(i(),r("div",ye,[t("div",_e,[s[7]||(s[7]=t("div",null,[t("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Functions "),t("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"},[c(" Every deployed handler on this Orva instance. Each function runs in its own nsjail sandbox and is reachable via "),t("code",{class:"font-mono text-[11px]"},"/fn/"),c(" or any custom route you've attached. ")])],-1)),d(F,{onClick:s[0]||(s[0]=e=>l(k).push("/functions/new"))},{default:L(()=>[d(l(B),{class:"w-4 h-4"}),s[6]||(s[6]=c(" New Function ",-1))]),_:1})]),m.value.length>0||x.value?(i(),r("div",we,[t("div",ke,[t("div",Ce,[d(l(ie),{class:"w-3.5 h-3.5 absolute left-2.5 top-1/2 -translate-y-1/2 text-foreground-muted/60 pointer-events-none"}),re(t("input",{"onUpdate:modelValue":s[1]||(s[1]=e=>v.value=e),placeholder:"Search by name, runtime, or function id…",class:"w-full bg-background border border-border rounded-md pl-8 pr-3 py-1.5 text-base sm:text-xs text-foreground placeholder-foreground-muted/60 focus:outline-none focus:border-white"},null,512),[[de,v.value]])]),t("span",$e,n(f.value.length)+" of "+n(m.value.length),1)]),t("div",De,[t("ul",Le,[(i(!0),r(j,null,A(f.value,e=>(i(),r("li",{key:e.id,class:"px-4 py-3 active:bg-surface-hover/50 transition-colors"},[t("div",Se,[t("div",Te,[t("div",Ee,[t("span",Re,n(e.name),1),e.network_mode==="egress"?(i(),r("span",Fe,[d(l(V),{class:"w-3 h-3"}),s[8]||(s[8]=c(" egress ",-1))])):u("",!0),e.auth_mode&&e.auth_mode!=="none"?(i(),r("span",Pe,[d(l(q),{class:"w-3 h-3"}),c(" "+n(e.auth_mode==="platform_key"?"key":"signed"),1)])):u("",!0)]),e.description?(i(),r("p",ze,n(e.description),1)):u("",!0),t("div",Ne,[t("span",null,n(e.runtime),1),t("span",null,n(e.cpus)+" CPU / "+n(e.memory_mb)+"MB",1)])]),t("div",Ue,[d(w,{icon:l(O),title:"Edit function",onClick:p=>l(k).push("/functions/"+e.name)},null,8,["icon","onClick"]),d(w,{icon:l(P),variant:"danger",title:"Delete function",disabled:C.value===e.id,onClick:p=>U(e)},null,8,["icon","disabled","onClick"])])])]))),128)),!f.value.length&&!x.value&&v.value?(i(),r("li",Ie,[t("div",null,'No matches for "'+n(v.value)+'".',1),t("button",{class:"text-xs text-foreground hover:text-white underline underline-offset-2",onClick:s[2]||(s[2]=e=>v.value="")},"Clear search")])):u("",!0)]),t("table",Me,[t("thead",je,[t("tr",null,[t("th",Ae,[t("input",{type:"checkbox",checked:R.value,".indeterminate":J.value&&!R.value,class:"w-3.5 h-3.5 rounded border-border bg-background",onChange:K},null,40,Be)]),s[9]||(s[9]=t("th",{class:"px-4 py-3 font-medium"}," Name ",-1)),s[10]||(s[10]=t("th",{class:"px-4 py-3 font-medium hidden sm:table-cell"}," Runtime ",-1)),s[11]||(s[11]=t("th",{class:"px-4 py-3 font-medium hidden lg:table-cell"}," Resources ",-1)),s[12]||(s[12]=t("th",{class:"px-4 py-3 font-medium hidden md:table-cell"}," Function ID ",-1)),s[13]||(s[13]=t("th",{class:"px-4 py-3 font-medium hidden xl:table-cell"}," Last Update ",-1)),s[14]||(s[14]=t("th",{class:"px-4 py-3 font-medium text-right"}," Actions ",-1))])]),t("tbody",Ge,[(i(!0),r(j,null,A(f.value,e=>(i(),r("tr",{key:e.id,class:xe(["hover:bg-surface/50 transition-colors",{"bg-surface/30":a.value.has(e.id)}])},[t("td",Ve,[t("input",{checked:a.value.has(e.id),type:"checkbox",class:"w-3.5 h-3.5 rounded border-border bg-background",onChange:p=>Z(e.id)},null,40,qe)]),t("td",Oe,[t("div",He,[t("span",null,n(e.name),1),e.network_mode==="egress"?(i(),r("span",Je,[d(l(V),{class:"w-3 h-3"}),s[15]||(s[15]=c(" egress ",-1))])):u("",!0),e.auth_mode&&e.auth_mode!=="none"?(i(),r("span",{key:1,class:"inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-[10px] bg-info-tint text-info-fg border border-info-ring",title:e.auth_mode==="platform_key"?"Requires Orva API key":"Requires HMAC signature"},[d(l(q),{class:"w-3 h-3"}),c(" "+n(e.auth_mode==="platform_key"?"key":"signed"),1)],8,Ze)):u("",!0),e.rate_limit_per_min>0?(i(),r("span",{key:2,class:"inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-[10px] bg-primary/15 text-primary border border-primary/30 tabular-nums",title:`Rate limit: ${e.rate_limit_per_min}/min per IP`},[d(l(ve),{class:"w-3 h-3"}),c(" "+n(e.rate_limit_per_min)+"/m ",1)],8,Ke)):u("",!0)]),e.description?(i(),r("p",{key:0,class:"mt-1 text-xs font-normal text-foreground-muted line-clamp-2",title:e.description},n(e.description),9,Qe)):u("",!0)]),t("td",We,[t("span",Xe,n(e.runtime),1)]),t("td",Ye,n(e.cpus)+" CPU / "+n(e.memory_mb)+"MB ",1),t("td",et,[t("div",tt,[t("code",{class:"text-xs font-mono text-foreground-muted bg-surface px-2 py-1 rounded border border-border truncate min-w-0 max-w-[14ch]",title:e.id},n(e.id),9,st),d(w,{icon:b.value===e.id?l(ge):l(be),title:b.value===e.id?"Copied!":"Copy invoke URL",variant:b.value===e.id?"primary":"default",onClick:p=>Q(e)},null,8,["icon","title","variant","onClick"])])]),t("td",ot,n(new Date(e.updated_at).toLocaleDateString()),1),t("td",nt,[t("div",lt,[d(w,{icon:l(O),title:"Edit function",onClick:p=>l(k).push("/functions/"+e.name)},null,8,["icon","onClick"]),d(w,{icon:l(P),variant:"danger",title:"Delete function",disabled:C.value===e.id,onClick:p=>U(e)},null,8,["icon","disabled","onClick"])])])],2))),128)),!f.value.length&&!x.value&&v.value?(i(),r("tr",at,[t("td",it,[t("div",rt,[t("div",null,'No matches for "'+n(v.value)+'".',1),t("button",{class:"text-xs text-foreground hover:text-white underline underline-offset-2",onClick:s[3]||(s[3]=e=>v.value="")},"Clear search")])])])):u("",!0)])]),H.value?(i(),r("div",dt,[t("button",{class:"text-xs text-foreground-muted hover:text-white transition-colors flex items-center gap-1.5",disabled:x.value,onClick:W},[x.value?(i(),ue(l(he),{key:0,class:"w-3 h-3 animate-spin"})):u("",!0),c(" "+n(x.value?"Loading…":`Load more (${T.value-m.value.length} remaining)`),1)],8,ut)])):u("",!0)])])):u("",!0),!x.value&&m.value.length===0?(i(),r("div",ct,[s[17]||(s[17]=t("div",{class:"space-y-1.5"},[t("div",{class:"text-sm text-white"},"No functions deployed yet"),t("div",{class:"text-xs text-foreground-muted max-w-prose mx-auto leading-body"},[c(" Each function runs in its own nsjail sandbox and is reachable at "),t("code",{class:"font-mono text-[11px]"},"/fn/"),c(" the moment it's deployed. Pick a runtime, paste your handler, hit Deploy. ")])],-1)),t("div",null,[d(F,{onClick:s[4]||(s[4]=e=>l(k).push("/functions/new"))},{default:L(()=>[d(l(B),{class:"w-4 h-4"}),s[16]||(s[16]=c(" Deploy your first function ",-1))]),_:1})])])):u("",!0),d(ce,{name:"fade"},{default:L(()=>[a.value.size?(i(),r("div",mt,[t("span",pt,n(a.value.size)+" selected ",1),s[18]||(s[18]=t("span",{class:"w-px h-4 bg-border"},null,-1)),t("button",{class:"text-xs text-foreground-muted hover:text-white transition-colors px-2 py-1",onClick:s[5]||(s[5]=e=>a.value=new Set)}," Clear "),d(F,{variant:"danger",size:"sm",class:"!rounded-full px-4",loading:E.value,onClick:X},{default:L(()=>[d(l(P),{class:"w-3.5 h-3.5"}),c(" Delete "+n(a.value.size),1)]),_:1},8,["loading"])])):u("",!0)]),_:1})]))}};export{$t as default}; diff --git a/backend/internal/server/ui_dist/assets/FunctionsList-D8VvPWAC.js b/backend/internal/server/ui_dist/assets/FunctionsList-D8VvPWAC.js new file mode 100644 index 0000000..55958d8 --- /dev/null +++ b/backend/internal/server/ui_dist/assets/FunctionsList-D8VvPWAC.js @@ -0,0 +1,3 @@ +import{G as ee,Y as te,o as se,y as oe,X as ne,Z as le,a as r,b as t,k as c,d,h as L,f as l,_ as R,$ as ie,e as ae,v as re,t as n,F as A,p as B,g as u,n as de,N as ue,a0 as ce,i as me,r as h,q as S,j as a,P as I,s as pe,a1 as xe,D as G}from"./index-CmY58qNN.js";import{_ as w}from"./IconButton-i2ilFBbf.js";import{c as fe}from"./clipboard-CmSw2rR-.js";import{R as ve}from"./refresh-cw-63KivPtM.js";import{G as V}from"./globe-D5WsrNJb.js";import{L as q}from"./lock-DHycfcno.js";import{P as O}from"./pencil-B6LJWpZU.js";import{T as F}from"./trash-2-BJzRpJ7Y.js";import{C as ge}from"./check-z8qyH5P-.js";import{C as he}from"./copy-BzujsGZw.js";const be={class:"space-y-6"},ye={class:"flex items-start justify-between gap-4 flex-wrap"},_e={key:0,class:"space-y-3"},we={class:"flex items-center gap-2 flex-wrap"},ke={class:"relative flex-1 min-w-0 sm:min-w-[280px] max-w-full sm:max-w-[440px]"},Ce={class:"text-[11px] text-foreground-muted shrink-0 tabular-nums"},$e={class:"bg-background border border-border rounded-lg overflow-x-auto"},De={class:"sm:hidden divide-y divide-border"},Le={class:"flex items-start justify-between gap-2"},Se={class:"min-w-0 flex-1"},Te={class:"flex items-center gap-1.5 flex-wrap"},Ee={class:"font-medium text-white truncate"},Pe={key:0,class:"inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-[10px] bg-warning-tint text-warning-fg border border-warning-ring"},Re={key:1,class:"inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-[10px] bg-info-tint text-info-fg border border-info-ring"},Fe={key:0,class:"mt-1 text-xs text-foreground-muted line-clamp-2"},Ne={class:"mt-1.5 flex items-center gap-3 text-[11px] text-foreground-muted font-mono"},Ue={class:"flex items-center gap-1 shrink-0"},ze={key:0,class:"px-6 py-8 text-center text-sm text-foreground-muted space-y-3"},Me={class:"hidden sm:table w-full text-sm text-left"},je={class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},Ae={class:"px-4 py-3 w-8"},Be=["checked",".indeterminate"],Ie={class:"divide-y divide-border"},Ge={class:"px-4 py-3 align-middle"},Ve=["checked","onChange"],qe={class:"px-4 py-3 font-medium text-white"},Oe={class:"flex items-center gap-2 flex-wrap"},Ze={key:0,class:"inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-[10px] bg-warning-tint text-warning-fg border border-warning-ring",title:"Outbound network enabled"},He=["title"],Xe=["title"],Ye=["title"],Je={class:"px-4 py-3 text-foreground hidden sm:table-cell"},Ke={class:"inline-flex items-center px-2 py-0.5 rounded text-xs border border-border bg-background text-foreground-muted font-mono"},Qe={class:"px-4 py-3 text-foreground-muted font-mono text-xs hidden lg:table-cell"},We={class:"px-4 py-3 hidden md:table-cell align-middle"},et={class:"flex items-center gap-2 min-w-0"},tt=["title"],st={class:"px-4 py-3 text-foreground-muted hidden xl:table-cell"},ot={class:"px-4 py-3 text-right"},nt={class:"inline-flex items-center gap-1"},lt={key:0},it={colspan:"7",class:"px-6 py-8 text-center text-foreground-muted"},at={class:"space-y-3"},rt={key:0,class:"flex justify-center border-t border-border py-3 bg-surface/30"},dt=["disabled"],ut={key:1,class:"bg-background border border-border rounded-lg p-8 text-center space-y-4"},ct={key:0,class:"fixed bottom-[calc(1rem+env(safe-area-inset-bottom,0px))] left-1/2 -translate-x-1/2 z-30 flex items-center gap-3 bg-background border border-border shadow-lg rounded-full pl-4 pr-2 py-2"},mt={class:"text-xs text-white"},pt=25,$t={__name:"FunctionsList",setup(xt){const y=ee(),k=me(),m=h([]),T=h(0),x=h(!1),f=h(""),b=h(""),C=h(""),E=h(!1),i=h(new Set),v=S(()=>{const o=f.value.trim().toLowerCase();return o?m.value.filter(s=>s.name?.toLowerCase().includes(o)||s.description?.toLowerCase().includes(o)||s.runtime?.toLowerCase().includes(o)||s.id?.toLowerCase().includes(o)):m.value}),Z=S(()=>m.value.lengthv.value.length>0&&v.value.every(o=>i.value.has(o.id))),H=S(()=>v.value.some(o=>i.value.has(o.id))),X=o=>{const s=new Set(i.value);s.has(o)?s.delete(o):s.add(o),i.value=s},Y=()=>{if(P.value)i.value=new Set;else{const o=new Set(i.value);v.value.forEach(s=>o.add(s.id)),i.value=o}},N=o=>`${window.location.origin}/fn/${o.id}`,J=async o=>{await fe(N(o))?(b.value=o.id,setTimeout(()=>{b.value===o.id&&(b.value="")},1500)):y.notify({title:"Copy failed",message:`Could not copy to clipboard. URL: + +`+N(o)})},_=async o=>{x.value=!0;try{const s=await ce({limit:pt,offset:o}),e=s.data.functions||[];T.value=s.data.total??e.length,o===0?m.value=e:m.value=[...m.value,...e]}catch(s){console.error(s)}finally{x.value=!1}},K=()=>_(m.value.length),U=()=>_(0),z=async o=>{if(await y.ask({title:`Delete "${o.name}"?`,message:"This is irreversible. Code, deployments, secrets, and routes for this function are removed.",confirmLabel:"Delete",danger:!0})){C.value=o.id;try{await G.delete(`/functions/${o.id}`),await U(),i.value.delete(o.id),i.value=new Set(i.value)}catch(e){const p=e.response?.data?.error?.message||e.message||"Delete failed";y.notify({title:"Delete failed",message:p,danger:!0})}finally{C.value=""}}},Q=async()=>{const o=i.value.size;if(!await y.ask({title:`Delete ${o} ${o===1?"function":"functions"}?`,message:"Each one is irreversible. Code, deployments, secrets, and routes are removed.",confirmLabel:`Delete ${o}`,danger:!0}))return;E.value=!0;const e=[...i.value];let p=0;try{for(const W of e)try{await G.delete(`/functions/${W}`)}catch{p++}i.value=new Set,await U(),p&&y.notify({title:"Some deletes failed",message:`${p} of ${e.length} could not be deleted.`,danger:!0})}finally{E.value=!1}},M=te();let g=null;const j=()=>{g||(g=setTimeout(()=>{g=null,_(0)},300))};let $=null,D=null;return se(()=>{_(0),$=M.subscribe("function",j),D=M.subscribe("deployment",j)}),oe(()=>{$&&($(),$=null),D&&(D(),D=null),g&&(clearTimeout(g),g=null)}),ne(()=>_(0)),le(()=>{g&&(clearTimeout(g),g=null)}),(o,s)=>(a(),r("div",be,[t("div",ye,[s[7]||(s[7]=t("div",null,[t("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Functions "),t("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"},[c(" Every deployed handler on this Orva instance. Each function runs in its own nsjail sandbox and is reachable via "),t("code",{class:"font-mono text-[11px]"},"/fn/"),c(" or any custom route you've attached. ")])],-1)),d(R,{onClick:s[0]||(s[0]=e=>l(k).push("/functions/new"))},{default:L(()=>[d(l(I),{class:"w-4 h-4"}),s[6]||(s[6]=c(" New Function ",-1))]),_:1})]),m.value.length>0||x.value?(a(),r("div",_e,[t("div",we,[t("div",ke,[d(l(ie),{class:"w-3.5 h-3.5 absolute left-2.5 top-1/2 -translate-y-1/2 text-foreground-muted/60 pointer-events-none"}),ae(t("input",{"onUpdate:modelValue":s[1]||(s[1]=e=>f.value=e),placeholder:"Search by name, runtime, or function id…",class:"w-full bg-background border border-border rounded-md pl-8 pr-3 py-1.5 text-base sm:text-xs text-foreground placeholder-foreground-muted/60 focus:outline-none focus:ring-1 focus:ring-white focus:border-white"},null,512),[[re,f.value]])]),t("span",Ce,n(v.value.length)+" of "+n(m.value.length),1)]),t("div",$e,[t("ul",De,[(a(!0),r(A,null,B(v.value,e=>(a(),r("li",{key:e.id,class:"px-4 py-3 active:bg-surface-hover/50 transition-colors"},[t("div",Le,[t("div",Se,[t("div",Te,[t("span",Ee,n(e.name),1),e.network_mode==="egress"?(a(),r("span",Pe,[d(l(V),{class:"w-3 h-3"}),s[8]||(s[8]=c(" egress ",-1))])):u("",!0),e.auth_mode&&e.auth_mode!=="none"?(a(),r("span",Re,[d(l(q),{class:"w-3 h-3"}),c(" "+n(e.auth_mode==="platform_key"?"key":"signed"),1)])):u("",!0)]),e.description?(a(),r("p",Fe,n(e.description),1)):u("",!0),t("div",Ne,[t("span",null,n(e.runtime),1),t("span",null,n(e.cpus)+" CPU / "+n(e.memory_mb)+"MB",1)])]),t("div",Ue,[d(w,{icon:l(O),title:"Edit function",onClick:p=>l(k).push("/functions/"+e.name)},null,8,["icon","onClick"]),d(w,{icon:l(F),variant:"danger",title:"Delete function",disabled:C.value===e.id,onClick:p=>z(e)},null,8,["icon","disabled","onClick"])])])]))),128)),!v.value.length&&!x.value&&f.value?(a(),r("li",ze,[t("div",null,'No matches for "'+n(f.value)+'".',1),t("button",{class:"text-xs text-foreground hover:text-white underline underline-offset-2",onClick:s[2]||(s[2]=e=>f.value="")},"Clear search")])):u("",!0)]),t("table",Me,[t("thead",je,[t("tr",null,[t("th",Ae,[t("input",{type:"checkbox",checked:P.value,".indeterminate":H.value&&!P.value,class:"w-3.5 h-3.5 rounded border-border bg-background focus:outline-none focus:ring-1 focus:ring-white",onChange:Y},null,40,Be)]),s[9]||(s[9]=t("th",{class:"px-4 py-3 font-medium"}," Name ",-1)),s[10]||(s[10]=t("th",{class:"px-4 py-3 font-medium hidden sm:table-cell"}," Runtime ",-1)),s[11]||(s[11]=t("th",{class:"px-4 py-3 font-medium hidden lg:table-cell"}," Resources ",-1)),s[12]||(s[12]=t("th",{class:"px-4 py-3 font-medium hidden md:table-cell"}," Function ID ",-1)),s[13]||(s[13]=t("th",{class:"px-4 py-3 font-medium hidden xl:table-cell"}," Last Update ",-1)),s[14]||(s[14]=t("th",{class:"px-4 py-3 font-medium text-right"}," Actions ",-1))])]),t("tbody",Ie,[(a(!0),r(A,null,B(v.value,e=>(a(),r("tr",{key:e.id,class:pe(["hover:bg-surface/50 transition-colors",{"bg-surface/30":i.value.has(e.id)}])},[t("td",Ge,[t("input",{checked:i.value.has(e.id),type:"checkbox",class:"w-3.5 h-3.5 rounded border-border bg-background focus:outline-none focus:ring-1 focus:ring-white",onChange:p=>X(e.id)},null,40,Ve)]),t("td",qe,[t("div",Oe,[t("span",null,n(e.name),1),e.network_mode==="egress"?(a(),r("span",Ze,[d(l(V),{class:"w-3 h-3"}),s[15]||(s[15]=c(" egress ",-1))])):u("",!0),e.auth_mode&&e.auth_mode!=="none"?(a(),r("span",{key:1,class:"inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-[10px] bg-info-tint text-info-fg border border-info-ring",title:e.auth_mode==="platform_key"?"Requires Orva API key":"Requires HMAC signature"},[d(l(q),{class:"w-3 h-3"}),c(" "+n(e.auth_mode==="platform_key"?"key":"signed"),1)],8,He)):u("",!0),e.rate_limit_per_min>0?(a(),r("span",{key:2,class:"inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-[10px] bg-primary/15 text-primary border border-primary/30 tabular-nums",title:`Rate limit: ${e.rate_limit_per_min}/min per IP`},[d(l(xe),{class:"w-3 h-3"}),c(" "+n(e.rate_limit_per_min)+"/m ",1)],8,Xe)):u("",!0)]),e.description?(a(),r("p",{key:0,class:"mt-1 text-xs font-normal text-foreground-muted line-clamp-2",title:e.description},n(e.description),9,Ye)):u("",!0)]),t("td",Je,[t("span",Ke,n(e.runtime),1)]),t("td",Qe,n(e.cpus)+" CPU / "+n(e.memory_mb)+"MB ",1),t("td",We,[t("div",et,[t("code",{class:"text-xs font-mono text-foreground-muted bg-surface px-2 py-1 rounded border border-border truncate min-w-0 max-w-[14ch]",title:e.id},n(e.id),9,tt),d(w,{icon:b.value===e.id?l(ge):l(he),title:b.value===e.id?"Copied!":"Copy invoke URL",variant:b.value===e.id?"primary":"default",onClick:p=>J(e)},null,8,["icon","title","variant","onClick"])])]),t("td",st,n(new Date(e.updated_at).toLocaleDateString()),1),t("td",ot,[t("div",nt,[d(w,{icon:l(O),title:"Edit function",onClick:p=>l(k).push("/functions/"+e.name)},null,8,["icon","onClick"]),d(w,{icon:l(F),variant:"danger",title:"Delete function",disabled:C.value===e.id,onClick:p=>z(e)},null,8,["icon","disabled","onClick"])])])],2))),128)),!v.value.length&&!x.value&&f.value?(a(),r("tr",lt,[t("td",it,[t("div",at,[t("div",null,'No matches for "'+n(f.value)+'".',1),t("button",{class:"text-xs text-foreground hover:text-white underline underline-offset-2",onClick:s[3]||(s[3]=e=>f.value="")},"Clear search")])])])):u("",!0)])]),Z.value?(a(),r("div",rt,[t("button",{class:"text-xs text-foreground-muted hover:text-white transition-colors flex items-center gap-1.5",disabled:x.value,onClick:K},[x.value?(a(),de(l(ve),{key:0,class:"w-3 h-3 animate-spin"})):u("",!0),c(" "+n(x.value?"Loading…":`Load more (${T.value-m.value.length} remaining)`),1)],8,dt)])):u("",!0)])])):u("",!0),!x.value&&m.value.length===0?(a(),r("div",ut,[s[17]||(s[17]=t("div",{class:"space-y-1.5"},[t("div",{class:"text-sm text-white"},"No functions deployed yet"),t("div",{class:"text-xs text-foreground-muted max-w-prose mx-auto leading-body"},[c(" Each function runs in its own nsjail sandbox and is reachable at "),t("code",{class:"font-mono text-[11px]"},"/fn/"),c(" the moment it's deployed. Pick a runtime, paste your handler, hit Deploy. ")])],-1)),t("div",null,[d(R,{onClick:s[4]||(s[4]=e=>l(k).push("/functions/new"))},{default:L(()=>[d(l(I),{class:"w-4 h-4"}),s[16]||(s[16]=c(" Deploy your first function ",-1))]),_:1})])])):u("",!0),d(ue,{name:"fade"},{default:L(()=>[i.value.size?(a(),r("div",ct,[t("span",mt,n(i.value.size)+" selected ",1),s[18]||(s[18]=t("span",{class:"w-px h-4 bg-border"},null,-1)),t("button",{class:"text-xs text-foreground-muted hover:text-white transition-colors px-2 py-1",onClick:s[5]||(s[5]=e=>i.value=new Set)}," Clear "),d(R,{variant:"danger",size:"sm",class:"!rounded-full px-4",loading:E.value,onClick:Q},{default:L(()=>[d(l(F),{class:"w-3.5 h-3.5"}),c(" Delete "+n(i.value.size),1)]),_:1},8,["loading"])])):u("",!0)]),_:1})]))}};export{$t as default}; diff --git a/backend/internal/server/ui_dist/assets/IconButton-CclydhPm.js b/backend/internal/server/ui_dist/assets/IconButton-CclydhPm.js deleted file mode 100644 index a077423..0000000 --- a/backend/internal/server/ui_dist/assets/IconButton-CclydhPm.js +++ /dev/null @@ -1 +0,0 @@ -import{j as r,a,n,a2 as s,s as i,q as c}from"./index-WXhXpu06.js";const u=["title","disabled","aria-label"],g={__name:"IconButton",props:{icon:{type:[Object,Function],required:!0},title:{type:String,required:!0},variant:{type:String,default:"default",validator:e=>["default","danger","success","primary"].includes(e)},disabled:Boolean},setup(e){const t=e,o=c(()=>{switch(t.variant){case"danger":return"text-foreground-muted hover:text-error hover:bg-surface-hover focus:ring-red-500";case"success":return"text-foreground-muted hover:text-success hover:bg-surface-hover focus:ring-green-500";case"primary":return"text-primary hover:text-primary-hover bg-primary/10 hover:bg-primary/15 focus:ring-primary";default:return"text-foreground-muted hover:text-foreground hover:bg-surface-hover focus:ring-primary"}});return(l,d)=>(r(),a("button",{type:"button",title:e.title,disabled:e.disabled,"aria-label":e.title,class:i(["inline-flex items-center justify-center h-7 w-7 rounded-md transition-colors touch-expand-iconbtn","focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-background","disabled:opacity-40 disabled:cursor-not-allowed",o.value])},[(r(),n(s(e.icon),{class:"w-3.5 h-3.5"}))],10,u))}};export{g as _}; diff --git a/backend/internal/server/ui_dist/assets/IconButton-i2ilFBbf.js b/backend/internal/server/ui_dist/assets/IconButton-i2ilFBbf.js new file mode 100644 index 0000000..e19a1fe --- /dev/null +++ b/backend/internal/server/ui_dist/assets/IconButton-i2ilFBbf.js @@ -0,0 +1 @@ +import{j as r,a,n,K as s,s as i,q as c}from"./index-CmY58qNN.js";const u=["title","disabled","aria-label"],g={__name:"IconButton",props:{icon:{type:[Object,Function],required:!0},title:{type:String,required:!0},variant:{type:String,default:"default",validator:e=>["default","danger","success","primary"].includes(e)},disabled:Boolean},setup(e){const t=e,o=c(()=>{switch(t.variant){case"danger":return"text-foreground-muted hover:text-danger-fg hover:bg-surface-hover focus:ring-red-500";case"success":return"text-foreground-muted hover:text-success hover:bg-surface-hover focus:ring-green-500";case"primary":return"text-primary hover:text-primary-hover bg-primary/10 hover:bg-primary/15 focus:ring-primary";default:return"text-foreground-muted hover:text-foreground hover:bg-surface-hover focus:ring-primary"}});return(d,l)=>(r(),a("button",{type:"button",title:e.title,disabled:e.disabled,"aria-label":e.title,class:i(["inline-flex items-center justify-center h-7 w-7 rounded-md transition-colors touch-expand-iconbtn","focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-background","disabled:opacity-40 disabled:cursor-not-allowed",o.value])},[(r(),n(s(e.icon),{class:"w-3.5 h-3.5"}))],10,u))}};export{g as _}; diff --git a/backend/internal/server/ui_dist/assets/InboundWebhooks-Dckvwp1-.js b/backend/internal/server/ui_dist/assets/InboundWebhooks-Dckvwp1-.js new file mode 100644 index 0000000..f3dcc16 --- /dev/null +++ b/backend/internal/server/ui_dist/assets/InboundWebhooks-Dckvwp1-.js @@ -0,0 +1,5 @@ +import{c as K,G as J,o as Q,a as u,b as t,k as d,d as l,h as p,t as a,_ as h,g,F as P,p as R,ar as Z,q as U,a5 as ee,r as v,ao as G,ab as te,j as c,f as y,s as B,P as se,e as T,v as j,V as oe,as as re,at as ae}from"./index-CmY58qNN.js";import{E as ne}from"./format-CsU4_SPu.js";import{_ as D}from"./IconButton-i2ilFBbf.js";import{D as L}from"./Drawer-B7DqDJXO.js";import{R as le}from"./refresh-cw-63KivPtM.js";import{T as z}from"./trash-2-BJzRpJ7Y.js";const M=K("send",[["path",{d:"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z",key:"1ffxy3"}],["path",{d:"m21.854 2.147-10.94 10.939",key:"12cjpa"}]]),ie={class:"space-y-6"},de={class:"flex items-start justify-between gap-4"},ue={class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"},ce={class:"flex items-center gap-2"},pe={class:"text-xs text-foreground-muted"},me={key:0,class:"rounded-lg border border-amber-500/40 bg-amber-500/10 p-4 space-y-3"},xe={class:"flex items-center justify-between gap-4"},fe={class:"text-xs space-y-2"},ge={class:"ml-2 font-mono text-white break-all"},be={class:"ml-2 font-mono text-white break-all"},he={class:"mt-1 bg-background border border-border rounded p-3 text-[11px] font-mono text-white whitespace-pre-wrap overflow-x-auto"},ve={class:"bg-background border border-border rounded-lg overflow-x-auto"},ye={class:"sm:hidden divide-y divide-border"},_e={class:"flex items-start justify-between gap-2"},we={class:"min-w-0 flex-1"},ke={class:"flex items-center gap-2 flex-wrap"},$e={class:"font-medium text-white truncate"},Ce={class:"mt-1 text-[11px] text-foreground-muted font-mono break-all"},Se={class:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-foreground-muted"},Te={class:"font-mono"},De={class:"flex items-center gap-1 shrink-0"},Oe={key:0,class:"px-6 py-12 text-center text-sm text-foreground-muted"},Be={class:"hidden sm:table w-full text-sm text-left"},je={class:"divide-y divide-border"},Ie={class:"px-4 py-3 font-medium text-white"},He={class:"flex flex-col"},Ve={class:"text-[10px] text-foreground-muted font-mono"},Ne={class:"px-4 py-3 font-mono text-xs text-foreground-muted hidden md:table-cell"},Ye={class:"break-all"},Pe={class:"px-4 py-3 hidden sm:table-cell"},Re={class:"inline-flex items-center px-2 py-0.5 rounded text-[11px] border bg-surface text-foreground-muted border-border font-mono"},Ue={class:"px-4 py-3 font-mono text-xs text-foreground-muted hidden lg:table-cell"},Ge={class:"px-4 py-3 hidden md:table-cell"},Le={class:"px-4 py-3 text-foreground-muted text-xs hidden lg:table-cell"},ze={class:"px-4 py-3 text-right"},Me={class:"inline-flex items-center gap-1"},Ae={key:0},Xe={class:"p-5 space-y-5 text-sm"},Ee={key:0,class:"text-xs text-danger-fg"},Fe={class:"flex items-center justify-end gap-2"},We={key:0,class:"p-5 space-y-5 text-sm"},qe={class:"text-xs text-foreground-muted"},Ke={key:0,class:"text-xs text-danger-fg"},Je={key:1,class:"space-y-1"},Qe={class:"text-xs uppercase tracking-wider text-foreground-muted"},Ze={class:"bg-background border border-border rounded p-3 text-[11px] font-mono text-white whitespace-pre-wrap overflow-x-auto"},et={class:"flex items-center justify-end gap-2"},it={__name:"InboundWebhooks",setup(tt){const A=te(),k=J(),O=U(()=>A.params.name),_=v(""),x=v([]),w=v(!1),$=v(!1),C=v(!1),m=v(null),b=U(()=>window.location.origin),n=G({open:!1,name:"",format:"hmac_sha256_hex",error:""}),r=G({open:!1,row:null,secret:"",body:'{"hello":"orva"}',error:"",response:null}),I=o=>o?new Date(o).toLocaleString():ne,S=async()=>{w.value=!0;try{_.value||(_.value=O.value);const o=await Z(_.value);x.value=o.data?.inbound_webhooks||[]}catch(o){console.error("load inbound webhooks failed",o),k.notify({title:"Failed to load inbound webhooks",message:o?.response?.data?.error?.message||o.message,danger:!0})}finally{w.value=!1}},X=()=>{n.name="",n.format="hmac_sha256_hex",n.error="",n.open=!0},E=async()=>{const o=n.name.trim();if(!o){n.error="Name is required";return}$.value=!0,n.error="";try{const e=await ae(_.value,{name:o,signature_format:n.format});m.value={...e.data.inbound_webhook,secret:e.data.secret,trigger_url:e.data.trigger_url},n.open=!1,await S()}catch(e){n.error=e?.response?.data?.error?.message||"Create failed"}finally{$.value=!1}},H=async o=>{if(await k.ask({title:"Delete inbound webhook?",message:`Trigger "${o.name}" (${o.id}) will stop accepting calls immediately. This cannot be undone.`,confirmLabel:"Delete",danger:!0}))try{await re(_.value,o.id),await S()}catch(i){k.notify({title:"Delete failed",message:i?.response?.data?.error?.message||i.message,danger:!0})}},V=o=>{r.row=o,r.secret="",r.body='{"hello":"orva"}',r.error="",r.response=null,r.open=!0},N=async(o,e)=>{const i=new TextEncoder,s=await crypto.subtle.importKey("raw",i.encode(o),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),f=await crypto.subtle.sign("HMAC",s,i.encode(e));return[...new Uint8Array(f)].map(q=>q.toString(16).padStart(2,"0")).join("")},F=async()=>{r.error="",r.response=null,C.value=!0;try{const o=r.row.signature_format;let e;if(o==="hmac_sha256_hex")e=await N(r.secret.trim(),r.body);else if(o==="github")e="sha256="+await N(r.secret.trim(),r.body);else{r.error=`Browser test only signs hmac_sha256_hex and github. For ${o}, use the CLI or curl with openssl.`;return}const i=b.value+"/webhook/"+r.row.id,s=await fetch(i,{method:"POST",headers:{"Content-Type":"application/json",[r.row.signature_header]:e},body:r.body}),f=await s.text();r.response={status:s.status,body:f}}catch(o){r.error=o.message||"Test failed"}finally{C.value=!1}},W=o=>{const e=b.value+o.trigger_url,i=o.signature_format;return i==="github"?[`BODY='{"hello":"orva"}'`,`SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "${o.secret}" | sed 's/^.* //')`,`curl -X POST "${e}" \\`,' -H "Content-Type: application/json" \\',` -H "${o.signature_header}: sha256=$SIG" \\`,' -d "$BODY"'].join(` +`):i==="stripe"?[`BODY='{"hello":"orva"}'`,"TS=$(date +%s)",`SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "${o.secret}" | sed 's/^.* //')`,`curl -X POST "${e}" \\`,' -H "Content-Type: application/json" \\',` -H "${o.signature_header}: t=$TS,v1=$SIG" \\`,' -d "$BODY"'].join(` +`):i==="slack"?[`BODY='{"hello":"orva"}'`,"TS=$(date +%s)",`SIG=$(printf 'v0:%s:%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "${o.secret}" | sed 's/^.* //')`,`curl -X POST "${e}" \\`,' -H "Content-Type: application/json" \\',' -H "X-Slack-Request-Timestamp: $TS" \\',` -H "${o.signature_header}: v0=$SIG" \\`,' -d "$BODY"'].join(` +`):[`BODY='{"hello":"orva"}'`,`SIG=$(printf '%s' "$BODY" | ${i==="hmac_sha256_base64"?`openssl dgst -sha256 -hmac "${o.secret}" -binary | base64`:`openssl dgst -sha256 -hmac "${o.secret}" | sed 's/^.* //'`})`,`curl -X POST "${e}" \\`,' -H "Content-Type: application/json" \\',` -H "${o.signature_header}: $SIG" \\`,' -d "$BODY"'].join(` +`)},Y=async o=>{try{await navigator.clipboard.writeText(o),k.notify({title:"Copied",message:"",danger:!1})}catch(e){console.warn("clipboard write failed",e)}};return Q(S),(o,e)=>{const i=ee("router-link");return c(),u("div",ie,[t("div",de,[t("div",null,[e[14]||(e[14]=t("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Inbound webhooks ",-1)),t("p",ue,[e[12]||(e[12]=d(" External services POST to a signed URL to fire ",-1)),l(i,{to:`/functions/${O.value}`,class:"text-white underline"},{default:p(()=>[d(a(O.value),1)]),_:1},8,["to"]),e[13]||(e[13]=d(" Set the secret here, configure it on the upstream service. ",-1))])]),t("div",ce,[t("span",pe,a(x.value.length)+" "+a(x.value.length===1?"trigger":"triggers"),1),l(h,{variant:"secondary",size:"sm",onClick:S},{default:p(()=>[l(y(le),{class:B(["w-3.5 h-3.5",{"animate-spin":w.value}])},null,8,["class"]),e[15]||(e[15]=d(" Refresh ",-1))]),_:1}),l(h,{size:"sm",onClick:e[0]||(e[0]=s=>X())},{default:p(()=>[l(y(se),{class:"w-3.5 h-3.5"}),e[16]||(e[16]=d(" New trigger ",-1))]),_:1})])]),m.value?(c(),u("div",me,[t("div",xe,[e[17]||(e[17]=t("div",{class:"text-sm text-amber-200 font-medium"}," Trigger created. Copy the secret now. It will not be shown again. ",-1)),t("button",{class:"text-xs text-amber-200/80 hover:text-white",onClick:e[1]||(e[1]=s=>m.value=null)}," Dismiss ")]),t("div",fe,[t("div",null,[e[18]||(e[18]=t("span",{class:"text-foreground-muted uppercase tracking-wider text-[10px]"},"URL",-1)),t("code",ge,a(b.value+m.value.trigger_url),1),t("button",{class:"ml-2 text-amber-200 hover:text-white",onClick:e[2]||(e[2]=s=>Y(b.value+m.value.trigger_url))}," Copy URL ")]),t("div",null,[e[19]||(e[19]=t("span",{class:"text-foreground-muted uppercase tracking-wider text-[10px]"},"Secret",-1)),t("code",be,a(m.value.secret),1),t("button",{class:"ml-2 text-amber-200 hover:text-white",onClick:e[3]||(e[3]=s=>Y(m.value.secret))}," Copy secret ")]),t("div",null,[e[20]||(e[20]=t("span",{class:"text-foreground-muted uppercase tracking-wider text-[10px]"},"Sample curl",-1)),t("pre",he,a(W(m.value)),1)])])])):g("",!0),t("div",ve,[t("ul",ye,[(c(!0),u(P,null,R(x.value,s=>(c(),u("li",{key:s.id,class:"px-4 py-3"},[t("div",_e,[t("div",we,[t("div",ke,[t("span",$e,a(s.name),1),t("span",{class:B(["inline-flex items-center px-1.5 py-0.5 rounded text-[10px] border",s.active?"bg-success/10 text-success border-success/30":"bg-surface text-foreground-muted border-border"])},a(s.active?"active":"paused"),3)]),t("div",Ce,a(b.value)+"/webhook/"+a(s.id),1),t("div",Se,[t("span",Te,a(s.signature_format),1),t("span",null,"created "+a(I(s.created_at)),1)])]),t("div",De,[l(D,{icon:y(M),variant:"success",title:"Send a test payload",onClick:f=>V(s)},null,8,["icon","onClick"]),l(D,{icon:y(z),variant:"danger",title:"Delete",onClick:f=>H(s)},null,8,["icon","onClick"])])])]))),128)),!w.value&&!x.value.length?(c(),u("li",Oe,[...e[21]||(e[21]=[d(" No inbound triggers yet. Tap ",-1),t("strong",null,"+ New trigger",-1),d(" to mint one. ",-1)])])):g("",!0)]),t("table",Be,[e[23]||(e[23]=t("thead",{class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},[t("tr",null,[t("th",{class:"px-4 py-3"},"Name"),t("th",{class:"px-4 py-3 hidden md:table-cell"},"URL"),t("th",{class:"px-4 py-3 hidden sm:table-cell"},"Format"),t("th",{class:"px-4 py-3 hidden lg:table-cell"},"Secret"),t("th",{class:"px-4 py-3 hidden md:table-cell"},"Active"),t("th",{class:"px-4 py-3 hidden lg:table-cell"},"Created"),t("th",{class:"px-4 py-3 text-right"},"Actions")])],-1)),t("tbody",je,[(c(!0),u(P,null,R(x.value,s=>(c(),u("tr",{key:s.id,class:"hover:bg-surface/50 transition-colors"},[t("td",Ie,[t("div",He,[t("span",null,a(s.name),1),t("span",Ve,a(s.id),1)])]),t("td",Ne,[t("span",Ye,a(b.value)+"/webhook/"+a(s.id),1)]),t("td",Pe,[t("span",Re,a(s.signature_format),1)]),t("td",Ue,a(s.secret_preview),1),t("td",Ge,[t("span",{class:B(["inline-flex items-center px-2 py-0.5 rounded text-[11px] border",s.active?"bg-success/10 text-success border-success/30":"bg-surface text-foreground-muted border-border"])},a(s.active?"active":"paused"),3)]),t("td",Le,a(I(s.created_at)),1),t("td",ze,[t("div",Me,[l(D,{icon:y(M),variant:"success",title:"Send a test payload",onClick:f=>V(s)},null,8,["icon","onClick"]),l(D,{icon:y(z),variant:"danger",title:"Delete",onClick:f=>H(s)},null,8,["icon","onClick"])])])]))),128)),!w.value&&!x.value.length?(c(),u("tr",Ae,[...e[22]||(e[22]=[t("td",{colspan:"7",class:"px-4 py-12 text-center text-foreground-muted text-sm"},[d(" No inbound triggers yet. Click "),t("strong",null,"+ New trigger"),d(" to mint one. ")],-1)])])):g("",!0)])])]),l(L,{modelValue:n.open,"onUpdate:modelValue":e[7]||(e[7]=s=>n.open=s),title:"New inbound webhook trigger",width:"560px"},{footer:p(()=>[t("div",Fe,[l(h,{variant:"ghost",size:"sm",onClick:e[6]||(e[6]=s=>n.open=!1)},{default:p(()=>[...e[28]||(e[28]=[d(" Cancel ",-1)])]),_:1}),l(h,{size:"sm",disabled:$.value||!n.name.trim(),loading:$.value,onClick:E},{default:p(()=>[...e[29]||(e[29]=[d(" Create ",-1)])]),_:1},8,["disabled","loading"])])]),default:p(()=>[t("div",Xe,[t("div",null,[e[24]||(e[24]=t("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"},"Name",-1)),T(t("input",{"onUpdate:modelValue":e[4]||(e[4]=s=>n.name=s),placeholder:"e.g. github-deploys",class:"mt-2 w-full bg-surface border border-border rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-white"},null,512),[[j,n.name]])]),t("div",null,[e[26]||(e[26]=t("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"},"Signature format",-1)),T(t("select",{"onUpdate:modelValue":e[5]||(e[5]=s=>n.format=s),class:"mt-2 w-full bg-surface border border-border rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-white"},[...e[25]||(e[25]=[t("option",{value:"hmac_sha256_hex"},"hmac_sha256_hex (default)",-1),t("option",{value:"hmac_sha256_base64"},"hmac_sha256_base64",-1),t("option",{value:"github"},"github (X-Hub-Signature-256)",-1),t("option",{value:"stripe"},"stripe (Stripe-Signature)",-1),t("option",{value:"slack"},"slack (X-Slack-Signature)",-1)])],512),[[oe,n.format]]),e[27]||(e[27]=t("p",{class:"text-[11px] text-foreground-muted mt-2"}," Pick the format your upstream service produces. The header name is stamped automatically; you can override on the row after creation. ",-1))]),n.error?(c(),u("div",Ee,a(n.error),1)):g("",!0)])]),_:1},8,["modelValue"]),l(L,{modelValue:r.open,"onUpdate:modelValue":e[11]||(e[11]=s=>r.open=s),title:`Test ${r.row?.name||"trigger"}`,width:"640px"},{footer:p(()=>[t("div",et,[l(h,{variant:"ghost",size:"sm",onClick:e[10]||(e[10]=s=>r.open=!1)},{default:p(()=>[...e[32]||(e[32]=[d(" Close ",-1)])]),_:1}),l(h,{size:"sm",disabled:C.value||!r.secret.trim(),loading:C.value,onClick:F},{default:p(()=>[...e[33]||(e[33]=[d(" Send test ",-1)])]),_:1},8,["disabled","loading"])])]),default:p(()=>[r.row?(c(),u("div",We,[t("p",qe," Paste the plaintext secret you captured when you created this trigger. Orva does not store the plaintext; it can only show the preview ("+a(r.row.secret_preview)+"). ",1),t("div",null,[e[30]||(e[30]=t("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"},"Secret",-1)),T(t("input",{"onUpdate:modelValue":e[8]||(e[8]=s=>r.secret=s),type:"password",placeholder:"paste 64-hex secret",spellcheck:"false",class:"mt-2 w-full bg-surface border border-border rounded px-3 py-2 text-sm text-white font-mono focus:outline-none focus:border-white"},null,512),[[j,r.secret]])]),t("div",null,[e[31]||(e[31]=t("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"},"Body (raw)",-1)),T(t("textarea",{"onUpdate:modelValue":e[9]||(e[9]=s=>r.body=s),rows:"6",spellcheck:"false",class:"mt-2 w-full bg-surface border border-border rounded p-3 text-xs text-white font-mono focus:outline-none focus:border-white"},null,512),[[j,r.body]])]),r.error?(c(),u("div",Ke,a(r.error),1)):g("",!0),r.response?(c(),u("div",Je,[t("span",Qe,"Response (HTTP "+a(r.response.status)+")",1),t("pre",Ze,a(r.response.body),1)])):g("",!0)])):g("",!0)]),_:1},8,["modelValue","title"])])}}};export{it as default}; diff --git a/backend/internal/server/ui_dist/assets/InboundWebhooks-N9x7iSNm.js b/backend/internal/server/ui_dist/assets/InboundWebhooks-N9x7iSNm.js deleted file mode 100644 index 7779688..0000000 --- a/backend/internal/server/ui_dist/assets/InboundWebhooks-N9x7iSNm.js +++ /dev/null @@ -1,5 +0,0 @@ -import{c as K,C as J,o as Q,a as u,b as t,k as d,d as l,h as p,t as a,_ as v,g,F as P,p as R,ak as Z,al as ee,q as U,U as te,r as y,ah as L,a1 as se,j as c,f as _,s as B,P as oe,e as D,v as j,R as re,am as ae,an as ne}from"./index-WXhXpu06.js";import{E as le}from"./format-CsU4_SPu.js";import{_ as O}from"./IconButton-CclydhPm.js";import{D as z}from"./Drawer-DFsQitq5.js";import{R as ie}from"./refresh-cw-CEunRyai.js";import{T as G}from"./trash-2-dFLn8UYZ.js";const M=K("send",[["path",{d:"M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z",key:"1ffxy3"}],["path",{d:"m21.854 2.147-10.94 10.939",key:"12cjpa"}]]),de={class:"space-y-6"},ue={class:"flex items-start justify-between gap-4"},ce={class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"},pe={class:"flex items-center gap-2"},me={class:"text-xs text-foreground-muted"},xe={key:0,class:"rounded-lg border border-amber-500/40 bg-amber-500/10 p-4 space-y-3"},fe={class:"flex items-center justify-between gap-4"},ge={class:"text-xs space-y-2"},be={class:"ml-2 font-mono text-white break-all"},he={class:"ml-2 font-mono text-white break-all"},ve={class:"mt-1 bg-background border border-border rounded p-3 text-[11px] font-mono text-white whitespace-pre-wrap overflow-x-auto"},ye={class:"bg-background border border-border rounded-lg overflow-x-auto"},_e={class:"sm:hidden divide-y divide-border"},we={class:"flex items-start justify-between gap-2"},ke={class:"min-w-0 flex-1"},$e={class:"flex items-center gap-2 flex-wrap"},Ce={class:"font-medium text-white truncate"},Se={class:"mt-1 text-[11px] text-foreground-muted font-mono break-all"},Te={class:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-foreground-muted"},De={class:"font-mono"},Oe={class:"flex items-center gap-1 shrink-0"},Be={key:0,class:"px-6 py-12 text-center text-sm text-foreground-muted"},je={class:"hidden sm:table w-full text-sm text-left"},Ie={class:"divide-y divide-border"},He={class:"px-4 py-3 font-medium text-white"},Ve={class:"flex flex-col"},Ne={class:"text-[10px] text-foreground-muted font-mono"},Ye={class:"px-4 py-3 font-mono text-xs text-foreground-muted hidden md:table-cell"},Pe={class:"break-all"},Re={class:"px-4 py-3 hidden sm:table-cell"},Ue={class:"inline-flex items-center px-2 py-0.5 rounded text-[11px] border bg-surface text-foreground-muted border-border font-mono"},Le={class:"px-4 py-3 font-mono text-xs text-foreground-muted hidden lg:table-cell"},ze={class:"px-4 py-3 hidden md:table-cell"},Ge={class:"px-4 py-3 text-foreground-muted text-xs hidden lg:table-cell"},Me={class:"px-4 py-3 text-right"},Ae={class:"inline-flex items-center gap-1"},Xe={key:0},Fe={class:"p-5 space-y-5 text-sm"},Ee={key:0,class:"text-xs text-error"},We={class:"flex items-center justify-end gap-2"},qe={key:0,class:"p-5 space-y-5 text-sm"},Ke={class:"text-xs text-foreground-muted"},Je={key:0,class:"text-xs text-error"},Qe={key:1,class:"space-y-1"},Ze={class:"text-xs uppercase tracking-wider text-foreground-muted"},et={class:"bg-background border border-border rounded p-3 text-[11px] font-mono text-white whitespace-pre-wrap overflow-x-auto"},tt={class:"flex items-center justify-end gap-2"},dt={__name:"InboundWebhooks",setup(st){const A=se(),$=J(),w=U(()=>A.params.name),b=y(""),x=y([]),k=y(!1),C=y(!1),S=y(!1),m=y(null),h=U(()=>window.location.origin),n=L({open:!1,name:"",format:"hmac_sha256_hex",error:""}),r=L({open:!1,row:null,secret:"",body:'{"hello":"orva"}',error:"",response:null}),I=o=>o?new Date(o).toLocaleString():le,T=async()=>{k.value=!0;try{if(!b.value)try{const e=await Z(w.value);b.value=e.data.id||e.data.function?.id||w.value}catch{b.value=w.value}const o=await ee(b.value);x.value=o.data?.inbound_webhooks||[]}catch(o){console.error("load inbound webhooks failed",o),$.notify({title:"Failed to load inbound webhooks",message:o?.response?.data?.error?.message||o.message,danger:!0})}finally{k.value=!1}},X=()=>{n.name="",n.format="hmac_sha256_hex",n.error="",n.open=!0},F=async()=>{const o=n.name.trim();if(!o){n.error="Name is required";return}C.value=!0,n.error="";try{const e=await ne(b.value,{name:o,signature_format:n.format});m.value={...e.data.inbound_webhook,secret:e.data.secret,trigger_url:e.data.trigger_url},n.open=!1,await T()}catch(e){n.error=e?.response?.data?.error?.message||"Create failed"}finally{C.value=!1}},H=async o=>{if(await $.ask({title:"Delete inbound webhook?",message:`Trigger "${o.name}" (${o.id}) will stop accepting calls immediately. This cannot be undone.`,confirmLabel:"Delete",danger:!0}))try{await ae(b.value,o.id),await T()}catch(i){$.notify({title:"Delete failed",message:i?.response?.data?.error?.message||i.message,danger:!0})}},V=o=>{r.row=o,r.secret="",r.body='{"hello":"orva"}',r.error="",r.response=null,r.open=!0},N=async(o,e)=>{const i=new TextEncoder,s=await crypto.subtle.importKey("raw",i.encode(o),{name:"HMAC",hash:"SHA-256"},!1,["sign"]),f=await crypto.subtle.sign("HMAC",s,i.encode(e));return[...new Uint8Array(f)].map(q=>q.toString(16).padStart(2,"0")).join("")},E=async()=>{r.error="",r.response=null,S.value=!0;try{const o=r.row.signature_format;let e;if(o==="hmac_sha256_hex")e=await N(r.secret.trim(),r.body);else if(o==="github")e="sha256="+await N(r.secret.trim(),r.body);else{r.error=`Browser test only signs hmac_sha256_hex and github. For ${o}, use the CLI or curl with openssl.`;return}const i=h.value+"/webhook/"+r.row.id,s=await fetch(i,{method:"POST",headers:{"Content-Type":"application/json",[r.row.signature_header]:e},body:r.body}),f=await s.text();r.response={status:s.status,body:f}}catch(o){r.error=o.message||"Test failed"}finally{S.value=!1}},W=o=>{const e=h.value+o.trigger_url,i=o.signature_format;return i==="github"?[`BODY='{"hello":"orva"}'`,`SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "${o.secret}" | sed 's/^.* //')`,`curl -X POST "${e}" \\`,' -H "Content-Type: application/json" \\',` -H "${o.signature_header}: sha256=$SIG" \\`,' -d "$BODY"'].join(` -`):i==="stripe"?[`BODY='{"hello":"orva"}'`,"TS=$(date +%s)",`SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "${o.secret}" | sed 's/^.* //')`,`curl -X POST "${e}" \\`,' -H "Content-Type: application/json" \\',` -H "${o.signature_header}: t=$TS,v1=$SIG" \\`,' -d "$BODY"'].join(` -`):i==="slack"?[`BODY='{"hello":"orva"}'`,"TS=$(date +%s)",`SIG=$(printf 'v0:%s:%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "${o.secret}" | sed 's/^.* //')`,`curl -X POST "${e}" \\`,' -H "Content-Type: application/json" \\',' -H "X-Slack-Request-Timestamp: $TS" \\',` -H "${o.signature_header}: v0=$SIG" \\`,' -d "$BODY"'].join(` -`):[`BODY='{"hello":"orva"}'`,`SIG=$(printf '%s' "$BODY" | ${i==="hmac_sha256_base64"?`openssl dgst -sha256 -hmac "${o.secret}" -binary | base64`:`openssl dgst -sha256 -hmac "${o.secret}" | sed 's/^.* //'`})`,`curl -X POST "${e}" \\`,' -H "Content-Type: application/json" \\',` -H "${o.signature_header}: $SIG" \\`,' -d "$BODY"'].join(` -`)},Y=async o=>{try{await navigator.clipboard.writeText(o),$.notify({title:"Copied",message:"",danger:!1})}catch(e){console.warn("clipboard write failed",e)}};return Q(T),(o,e)=>{const i=te("router-link");return c(),u("div",de,[t("div",ue,[t("div",null,[e[14]||(e[14]=t("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Inbound webhooks ",-1)),t("p",ce,[e[12]||(e[12]=d(" External services POST to a signed URL to fire ",-1)),l(i,{to:`/functions/${w.value}`,class:"text-white underline"},{default:p(()=>[d(a(w.value),1)]),_:1},8,["to"]),e[13]||(e[13]=d(" Set the secret here, configure it on the upstream service. ",-1))])]),t("div",pe,[t("span",me,a(x.value.length)+" "+a(x.value.length===1?"trigger":"triggers"),1),l(v,{variant:"secondary",size:"sm",onClick:T},{default:p(()=>[l(_(ie),{class:B(["w-3.5 h-3.5",{"animate-spin":k.value}])},null,8,["class"]),e[15]||(e[15]=d(" Refresh ",-1))]),_:1}),l(v,{size:"sm",onClick:e[0]||(e[0]=s=>X())},{default:p(()=>[l(_(oe),{class:"w-3.5 h-3.5"}),e[16]||(e[16]=d(" New trigger ",-1))]),_:1})])]),m.value?(c(),u("div",xe,[t("div",fe,[e[17]||(e[17]=t("div",{class:"text-sm text-amber-200 font-medium"}," Trigger created. Copy the secret now. It will not be shown again. ",-1)),t("button",{class:"text-xs text-amber-200/80 hover:text-white",onClick:e[1]||(e[1]=s=>m.value=null)}," Dismiss ")]),t("div",ge,[t("div",null,[e[18]||(e[18]=t("span",{class:"text-foreground-muted uppercase tracking-wider text-[10px]"},"URL",-1)),t("code",be,a(h.value+m.value.trigger_url),1),t("button",{class:"ml-2 text-amber-200 hover:text-white",onClick:e[2]||(e[2]=s=>Y(h.value+m.value.trigger_url))}," Copy URL ")]),t("div",null,[e[19]||(e[19]=t("span",{class:"text-foreground-muted uppercase tracking-wider text-[10px]"},"Secret",-1)),t("code",he,a(m.value.secret),1),t("button",{class:"ml-2 text-amber-200 hover:text-white",onClick:e[3]||(e[3]=s=>Y(m.value.secret))}," Copy secret ")]),t("div",null,[e[20]||(e[20]=t("span",{class:"text-foreground-muted uppercase tracking-wider text-[10px]"},"Sample curl",-1)),t("pre",ve,a(W(m.value)),1)])])])):g("",!0),t("div",ye,[t("ul",_e,[(c(!0),u(P,null,R(x.value,s=>(c(),u("li",{key:s.id,class:"px-4 py-3"},[t("div",we,[t("div",ke,[t("div",$e,[t("span",Ce,a(s.name),1),t("span",{class:B(["inline-flex items-center px-1.5 py-0.5 rounded text-[10px] border",s.active?"bg-success/10 text-success border-success/30":"bg-surface text-foreground-muted border-border"])},a(s.active?"active":"paused"),3)]),t("div",Se,a(h.value)+"/webhook/"+a(s.id),1),t("div",Te,[t("span",De,a(s.signature_format),1),t("span",null,"created "+a(I(s.created_at)),1)])]),t("div",Oe,[l(O,{icon:_(M),variant:"success",title:"Send a test payload",onClick:f=>V(s)},null,8,["icon","onClick"]),l(O,{icon:_(G),variant:"danger",title:"Delete",onClick:f=>H(s)},null,8,["icon","onClick"])])])]))),128)),!k.value&&!x.value.length?(c(),u("li",Be,[...e[21]||(e[21]=[d(" No inbound triggers yet. Tap ",-1),t("strong",null,"+ New trigger",-1),d(" to mint one. ",-1)])])):g("",!0)]),t("table",je,[e[23]||(e[23]=t("thead",{class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},[t("tr",null,[t("th",{class:"px-4 py-3"},"Name"),t("th",{class:"px-4 py-3 hidden md:table-cell"},"URL"),t("th",{class:"px-4 py-3 hidden sm:table-cell"},"Format"),t("th",{class:"px-4 py-3 hidden lg:table-cell"},"Secret"),t("th",{class:"px-4 py-3 hidden md:table-cell"},"Active"),t("th",{class:"px-4 py-3 hidden lg:table-cell"},"Created"),t("th",{class:"px-4 py-3 text-right"},"Actions")])],-1)),t("tbody",Ie,[(c(!0),u(P,null,R(x.value,s=>(c(),u("tr",{key:s.id,class:"hover:bg-surface/50 transition-colors"},[t("td",He,[t("div",Ve,[t("span",null,a(s.name),1),t("span",Ne,a(s.id),1)])]),t("td",Ye,[t("span",Pe,a(h.value)+"/webhook/"+a(s.id),1)]),t("td",Re,[t("span",Ue,a(s.signature_format),1)]),t("td",Le,a(s.secret_preview),1),t("td",ze,[t("span",{class:B(["inline-flex items-center px-2 py-0.5 rounded text-[11px] border",s.active?"bg-success/10 text-success border-success/30":"bg-surface text-foreground-muted border-border"])},a(s.active?"active":"paused"),3)]),t("td",Ge,a(I(s.created_at)),1),t("td",Me,[t("div",Ae,[l(O,{icon:_(M),variant:"success",title:"Send a test payload",onClick:f=>V(s)},null,8,["icon","onClick"]),l(O,{icon:_(G),variant:"danger",title:"Delete",onClick:f=>H(s)},null,8,["icon","onClick"])])])]))),128)),!k.value&&!x.value.length?(c(),u("tr",Xe,[...e[22]||(e[22]=[t("td",{colspan:"7",class:"px-4 py-12 text-center text-foreground-muted text-sm"},[d(" No inbound triggers yet. Click "),t("strong",null,"+ New trigger"),d(" to mint one. ")],-1)])])):g("",!0)])])]),l(z,{modelValue:n.open,"onUpdate:modelValue":e[7]||(e[7]=s=>n.open=s),title:"New inbound webhook trigger",width:"560px"},{footer:p(()=>[t("div",We,[l(v,{variant:"ghost",size:"sm",onClick:e[6]||(e[6]=s=>n.open=!1)},{default:p(()=>[...e[28]||(e[28]=[d(" Cancel ",-1)])]),_:1}),l(v,{size:"sm",disabled:C.value||!n.name.trim(),loading:C.value,onClick:F},{default:p(()=>[...e[29]||(e[29]=[d(" Create ",-1)])]),_:1},8,["disabled","loading"])])]),default:p(()=>[t("div",Fe,[t("div",null,[e[24]||(e[24]=t("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"},"Name",-1)),D(t("input",{"onUpdate:modelValue":e[4]||(e[4]=s=>n.name=s),placeholder:"e.g. github-deploys",class:"mt-2 w-full bg-surface border border-border rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-white"},null,512),[[j,n.name]])]),t("div",null,[e[26]||(e[26]=t("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"},"Signature format",-1)),D(t("select",{"onUpdate:modelValue":e[5]||(e[5]=s=>n.format=s),class:"mt-2 w-full bg-surface border border-border rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-white"},[...e[25]||(e[25]=[t("option",{value:"hmac_sha256_hex"},"hmac_sha256_hex (default)",-1),t("option",{value:"hmac_sha256_base64"},"hmac_sha256_base64",-1),t("option",{value:"github"},"github (X-Hub-Signature-256)",-1),t("option",{value:"stripe"},"stripe (Stripe-Signature)",-1),t("option",{value:"slack"},"slack (X-Slack-Signature)",-1)])],512),[[re,n.format]]),e[27]||(e[27]=t("p",{class:"text-[11px] text-foreground-muted mt-2"}," Pick the format your upstream service produces. The header name is stamped automatically; you can override on the row after creation. ",-1))]),n.error?(c(),u("div",Ee,a(n.error),1)):g("",!0)])]),_:1},8,["modelValue"]),l(z,{modelValue:r.open,"onUpdate:modelValue":e[11]||(e[11]=s=>r.open=s),title:`Test ${r.row?.name||"trigger"}`,width:"640px"},{footer:p(()=>[t("div",tt,[l(v,{variant:"ghost",size:"sm",onClick:e[10]||(e[10]=s=>r.open=!1)},{default:p(()=>[...e[32]||(e[32]=[d(" Close ",-1)])]),_:1}),l(v,{size:"sm",disabled:S.value||!r.secret.trim(),loading:S.value,onClick:E},{default:p(()=>[...e[33]||(e[33]=[d(" Send test ",-1)])]),_:1},8,["disabled","loading"])])]),default:p(()=>[r.row?(c(),u("div",qe,[t("p",Ke," Paste the plaintext secret you captured when you created this trigger. Orva does not store the plaintext; it can only show the preview ("+a(r.row.secret_preview)+"). ",1),t("div",null,[e[30]||(e[30]=t("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"},"Secret",-1)),D(t("input",{"onUpdate:modelValue":e[8]||(e[8]=s=>r.secret=s),type:"password",placeholder:"paste 64-hex secret",spellcheck:"false",class:"mt-2 w-full bg-surface border border-border rounded px-3 py-2 text-sm text-white font-mono focus:outline-none focus:border-white"},null,512),[[j,r.secret]])]),t("div",null,[e[31]||(e[31]=t("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"},"Body (raw)",-1)),D(t("textarea",{"onUpdate:modelValue":e[9]||(e[9]=s=>r.body=s),rows:"6",spellcheck:"false",class:"mt-2 w-full bg-surface border border-border rounded p-3 text-xs text-white font-mono focus:outline-none focus:border-white"},null,512),[[j,r.body]])]),r.error?(c(),u("div",Je,a(r.error),1)):g("",!0),r.response?(c(),u("div",Qe,[t("span",Ze,"Response (HTTP "+a(r.response.status)+")",1),t("pre",et,a(r.response.body),1)])):g("",!0)])):g("",!0)]),_:1},8,["modelValue","title"])])}}};export{dt as default}; diff --git a/backend/internal/server/ui_dist/assets/Input-B5GdDd-T.js b/backend/internal/server/ui_dist/assets/Input-B5GdDd-T.js new file mode 100644 index 0000000..0d0d420 --- /dev/null +++ b/backend/internal/server/ui_dist/assets/Input-B5GdDd-T.js @@ -0,0 +1 @@ +import{j as t,a,k as u,t as o,g as l,b as n,s as i,n as c,K as m}from"./index-CmY58qNN.js";const f={class:"flex flex-col gap-1.5 w-full"},g={key:0,class:"text-xs font-medium text-foreground-muted uppercase tracking-wide"},x={key:0,class:"text-danger-fg"},y={class:"relative"},b=["type","value","placeholder","disabled"],h={key:0,class:"absolute left-3 top-1/2 -translate-y-1/2 text-foreground-muted"},k={key:1,class:"text-xs text-red-500"},p={key:2,class:"text-xs text-foreground-muted"},S={__name:"Input",props:{modelValue:{type:[String,Number],default:""},label:{type:String,default:""},type:{type:String,default:"text"},placeholder:{type:String,default:""},error:{type:String,default:""},hint:{type:String,default:""},icon:{type:Object,default:null},required:Boolean,disabled:Boolean},emits:["update:modelValue"],setup(e){return(d,r)=>(t(),a("div",f,[e.label?(t(),a("label",g,[u(o(e.label)+" ",1),e.required?(t(),a("span",x,"*")):l("",!0)])):l("",!0),n("div",y,[n("input",{type:e.type,value:e.modelValue,class:i(["w-full bg-background border border-border rounded-md px-3 py-2 text-base sm:text-sm text-foreground placeholder-foreground-muted/50 focus:outline-none focus:ring-1 focus:ring-white focus:border-white transition-colors duration-200",{"pl-9":e.icon}]),placeholder:e.placeholder,disabled:e.disabled,onInput:r[0]||(r[0]=s=>d.$emit("update:modelValue",s.target.value))},null,42,b),e.icon?(t(),a("div",h,[(t(),c(m(e.icon),{class:"w-4 h-4"}))])):l("",!0)]),e.error?(t(),a("span",k,o(e.error),1)):l("",!0),e.hint&&!e.error?(t(),a("span",p,o(e.hint),1)):l("",!0)]))}};export{S as _}; diff --git a/backend/internal/server/ui_dist/assets/InvocationsLog-4RoFVxpq.js b/backend/internal/server/ui_dist/assets/InvocationsLog-4RoFVxpq.js new file mode 100644 index 0000000..08baf12 --- /dev/null +++ b/backend/internal/server/ui_dist/assets/InvocationsLog-4RoFVxpq.js @@ -0,0 +1,5 @@ +import{G as je,I as Ue,o as He,X as Ge,Z as Je,y as Ke,a as r,b as o,d as u,h as S,_ as R,f as c,$ as Xe,e as fe,v as Ze,V as Ye,F as N,p as M,g as v,t as l,n as pe,k as y,N as Qe,a0 as We,aE as me,r as f,aF as et,z as w,q as k,i as tt,j as a,s as z,w as J,aG as st,aH as ot,aI as at,aJ as rt,D as nt,aK as lt,aL as ut,aM as it}from"./index-CmY58qNN.js";import{E as T}from"./format-CsU4_SPu.js";import{D as dt}from"./Drawer-B7DqDJXO.js";import{_ as re}from"./StatusBadge-MvZdFvHS.js";import{c as ct}from"./clipboard-CmSw2rR-.js";import{c as vt}from"./aiPrompts-Dgb3jxRL.js";import{C as xe}from"./circle-alert-zkHrlU5I.js";import{R as K}from"./refresh-cw-63KivPtM.js";import{C as ft}from"./chevron-down-BMYhN6hn.js";import{C as pt}from"./check-z8qyH5P-.js";import{T as mt}from"./trash-2-BJzRpJ7Y.js";import{R as xt}from"./rotate-ccw-DQrtkmfg.js";import{P as gt}from"./play-DZQVXk9D.js";import{S as bt}from"./sparkles-B3_cifRe.js";import"./circle-DeGmlwna.js";import"./clock-CbnKorJ0.js";import"./circle-check-Z_CRCWca.js";const ht={class:"space-y-6"},yt={class:"flex items-center justify-between"},_t={class:"flex items-center gap-2 flex-wrap"},wt={class:"relative flex-1 min-w-[280px] max-w-[440px]"},kt=["value"],Ct={key:0,class:"bg-background border border-border rounded-lg px-6 py-12 text-center"},St={class:"mt-1 text-xs text-foreground-muted"},Tt={key:1,class:"bg-background border border-border rounded-lg overflow-x-auto"},Dt={class:"sm:hidden divide-y divide-border"},Ft=["onClick"],It={class:"flex items-start gap-3"},Rt=["checked","onChange"],qt={class:"min-w-0 flex-1"},$t={class:"flex items-center justify-between gap-2"},Vt={class:"font-medium text-white truncate"},Et={class:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-foreground-muted"},Lt={key:0,class:"font-mono"},Pt={key:1,class:"font-mono"},Nt={key:2,class:"inline-flex items-center px-1.5 py-0.5 rounded text-[10px] border bg-background font-mono text-info-fg border-info-ring"},Mt={key:0,class:"mt-1 text-[11px] text-foreground-muted font-mono break-all"},zt={key:0,class:"px-6 py-12 text-center text-sm text-foreground-muted"},At={class:"hidden sm:table w-full text-sm text-left"},Bt={class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},Ot={class:"px-4 py-3 w-8"},jt=["checked"],Ut={class:"divide-y divide-border"},Ht=["onClick"],Gt=["checked","onChange"],Jt={class:"px-4 py-3 text-foreground"},Kt={class:"px-4 py-3 font-medium text-white"},Xt=["onClick"],Zt={class:"px-4 py-3"},Yt={class:"px-4 py-3 hidden md:table-cell"},Qt={key:0,class:"inline-flex items-center px-2 py-0.5 rounded text-xs border bg-background font-mono text-info-fg border-info-ring"},Wt={key:1,class:"text-foreground-muted text-xs"},es={class:"px-4 py-3 text-foreground-muted font-mono text-xs hidden lg:table-cell"},ts={class:"px-4 py-3 text-foreground-muted font-mono text-xs hidden sm:table-cell"},ss={class:"px-4 py-3 hidden lg:table-cell"},os=["title","onClick"],as={key:1,class:"text-foreground-muted"},rs={class:"px-4 py-3 text-right text-foreground-muted font-mono text-xs hidden xl:table-cell"},ns={key:0},ls={colspan:"9",class:"px-6 py-8 text-center text-foreground-muted"},us={key:0,class:"flex justify-center border-t border-border py-3 bg-surface/30"},is=["disabled"],ds={key:0,class:"fixed bottom-4 left-1/2 -translate-x-1/2 z-30 flex items-center gap-3 bg-background border border-border shadow-lg rounded-full pl-4 pr-2 py-2"},cs={class:"text-xs text-white"},vs={key:0,class:"p-6 text-sm text-foreground-muted"},fs={key:1,class:"p-8 text-center"},ps={class:"mt-1 text-xs text-foreground-muted"},ms={key:2,class:"p-6 text-sm text-foreground-muted"},xs={key:3,class:"p-5 space-y-5"},gs={class:"flex items-center gap-2 flex-wrap"},bs={key:0,class:"inline-flex items-center px-2.5 py-1 rounded text-xs border bg-background font-mono text-info-fg border-info-ring"},hs={key:1,class:"inline-flex items-center px-2.5 py-1 rounded text-xs border bg-background font-mono text-foreground-muted"},ys={class:"grid grid-cols-2 gap-3 text-sm"},_s={key:0},ws={class:"bg-danger-tint border border-danger-ring rounded p-3 text-xs text-danger-fg font-mono whitespace-pre-wrap break-words"},ks={key:1},Cs={class:"bg-surface border border-border rounded p-3 space-y-3"},Ss={class:"flex items-center gap-2 font-mono text-xs"},Ts={class:"px-2 py-0.5 rounded bg-background text-white border border-border"},Ds={class:"text-foreground-muted truncate"},Fs={key:0},Is={class:"bg-background border border-border rounded p-2 max-h-40 overflow-auto"},Rs={class:"text-foreground-muted shrink-0"},qs={key:1},$s={class:"bg-background border border-border rounded p-2 text-xs text-foreground font-mono overflow-auto max-h-40 whitespace-pre-wrap break-words"},Vs={key:2,class:"text-[11px] text-warning-fg"},Es={key:2},Ls={class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},Ps={class:"bg-surface border border-border rounded p-3 text-xs font-mono space-y-1 max-h-72 overflow-auto"},Ns={class:"text-foreground-muted text-[10px] tabular-nums"},Ms={class:"text-white truncate"},zs={key:0,class:"text-[10px] text-foreground-muted truncate"},As={class:"flex items-center justify-between mb-2"},Bs={class:"bg-surface border border-border rounded p-3 text-xs text-foreground font-mono overflow-auto max-h-72 whitespace-pre-wrap break-words"},Os={class:"pt-2 border-t border-border flex items-center gap-3"},js={key:1,class:"text-xs text-foreground-muted"},Us=50,io={__name:"InvocationsLog",setup(Hs){const C=je(),X=tt(),x=f([]),A=f(0),b=f(!1),B=f(""),Z=f(!1),O=f(!1),Y=f(!1),j=f(""),n=f(null),m=f(new Set),_=f(""),U=f([]),ge=t=>{switch(t){case"error":return"text-danger-fg";case"warn":return"text-warning-fg";case"debug":return"text-foreground-muted";default:return"text-primary-light"}},be=t=>{if(!t)return"";try{const e=new Date(t);return e.toLocaleTimeString(void 0,{hour12:!1})+"."+String(e.getMilliseconds()).padStart(3,"0")}catch{return t}},H=f(!1),Q=f({}),d=f(null),F=f(!1),q=f(!1),$=f(!1),D=f(null);let V=null;const he=k(()=>x.value.lengthx.value.length>0&&x.value.every(t=>m.value.has(t.id))),ye=k(()=>x.value.some(t=>m.value.has(t.id))),ee=f(null);Ue(()=>ye.value&&!W.value,t=>{ee.value&&(ee.value.indeterminate=t)},{immediate:!0});const ne=t=>{const e=new Set(m.value);e.has(t)?e.delete(t):e.add(t),m.value=e},_e=()=>{if(W.value)m.value=new Set;else{const t=new Set(m.value);x.value.forEach(e=>t.add(e.id)),m.value=t}},p=f({fnId:"",status:"",range:"",q:""}),we=[{value:"",label:"All"},{value:"success",label:"Success"},{value:"error",label:"Error"}],ke=[{value:"",label:"All time"},{value:"1h",label:"1h"},{value:"24h",label:"24h"},{value:"7d",label:"7d"}],te=k(()=>!!(p.value.fnId||p.value.status||p.value.range||p.value.q)),Ce=()=>{p.value={fnId:"",status:"",range:"",q:""},h()};let se=null;const Se=()=>{se&&clearTimeout(se),se=setTimeout(()=>h(),300)},oe=()=>h(),Te=t=>{if(!t)return"";const e={"1h":36e5,"24h":864e5,"7d":7*864e5}[t];return e?new Date(Date.now()-e).toISOString():""},De=k(()=>n.value?`Invocation · ${n.value.id?.substring(0,14)}`:"Invocation"),Fe=k(()=>F.value?"request not captured":d.value?.truncated?"body was truncated; replay would be inaccurate":"Re-run this exact request against the current code"),Ie=k(()=>{const t=n.value;return t?typeof t.status_code=="number"&&t.status_code>=500?!0:!!t.error_message:!1}),Re=k(()=>_.value?"Build a paste-ready debug prompt with source + request + stderr":"no stderr to debug from"),I={props:{label:String,value:[String,Number],mono:Boolean},template:` +
+
{{ label }}
+
{{ value }}
+
`},le=et({name:"FilterChip",props:{options:{type:Array,required:!0},modelValue:{type:String,default:""},label:{type:String,required:!0}},emits:["update:modelValue"],setup(t,{emit:e}){const s=f(!1),i=k(()=>t.options.find(g=>g.value===t.modelValue&&g.value!=="")),P=()=>{s.value=!1},Ae=g=>{e("update:modelValue",g),P()},Be=g=>{g.stopPropagation(),e("update:modelValue","")},Oe=g=>{g.target.closest(".fc-root")||P()};return()=>w("div",{class:"fc-root relative",onMouseenter:()=>{document.addEventListener("mousedown",Oe)},onMouseleave:()=>{}},[w("button",{class:["inline-flex items-center gap-1.5 rounded-md border h-7 px-2.5 text-xs transition-colors",i.value?"bg-primary text-primary-foreground border-primary":"bg-surface text-foreground-muted border-border hover:text-white hover:border-foreground-muted"],onClick:()=>{s.value=!s.value}},[w("span",{class:"text-[10px] uppercase tracking-wider"},t.label+(i.value?":":"")),i.value?w("span",null,i.value.label):null,i.value?w("span",{class:"opacity-70 hover:opacity-100 -mr-0.5",onClick:Be,title:"Clear"},"✕"):w(ft,{class:"w-3 h-3 opacity-60"})]),s.value?w("div",{class:"absolute z-30 mt-1 left-0 min-w-[140px] bg-background border border-border rounded-md shadow-xl py-1"},t.options.filter(g=>g.value!=="").map(g=>w("button",{key:g.value,class:["w-full text-left px-2.5 py-1.5 text-xs flex items-center gap-2 transition-colors",g.value===t.modelValue?"text-white":"text-foreground-muted hover:text-white hover:bg-surface-hover"],onClick:()=>Ae(g.value)},[w(pt,{class:["w-3 h-3",g.value===t.modelValue?"opacity-100":"opacity-0"]}),g.label]))):null])}}),G=t=>t?new Date(t).toLocaleString():T,qe=t=>t<1024?`${t} B`:t<1024*1024?`${(t/1024).toFixed(1)} KB`:`${(t/1024/1024).toFixed(1)} MB`,E=t=>Q.value[t]||t?.slice(0,12)||"?",$e=async()=>{try{((await We()).data.functions||[]).forEach(e=>Q.value[e.id]=e.name)}catch{}},ue=t=>{const e={limit:Us,offset:t};return p.value.fnId&&(e.function_id=p.value.fnId),p.value.status&&(e.status=p.value.status),p.value.range&&(e.since=Te(p.value.range)),p.value.q&&(e.q=p.value.q),e},h=async()=>{b.value=!0;try{const t=await me(ue(0));x.value=t.data.executions||[],A.value=t.data.total??x.value.length,B.value=""}catch(t){console.error("Failed to fetch logs:",t),B.value=t?.response?.data?.error?.message||t.message||"Request failed"}finally{b.value=!1}},Ve=async()=>{b.value=!0;try{const t=await me(ue(x.value.length));x.value=[...x.value,...t.data.executions||[]],A.value=t.data.total??x.value.length}catch(t){console.error("Failed to load more:",t)}finally{b.value=!1}},ie=()=>h(),Ee=async()=>{const t=m.value.size;if(!await C.ask({title:`Delete ${t} ${t===1?"invocation":"invocations"}?`,message:"Removes the rows + their stderr logs. This is irreversible.",confirmLabel:`Delete ${t}`,danger:!0}))return;Z.value=!0;const s=[...m.value];try{const i=await nt.post("/executions/bulk-delete",{ids:s});m.value=new Set,await h(),i.data.failed&&C.notify({title:"Some deletes failed",message:`${i.data.failed} of ${s.length} could not be deleted.`,danger:!0})}catch(i){C.notify({title:"Bulk delete failed",message:i.response?.data?.error?.message||i.message,danger:!0})}finally{Z.value=!1}},ae=f(null),L=async t=>{ae.value=t,n.value=t,O.value=!0,Y.value=!0,j.value="",_.value="",H.value=!1,d.value=null,F.value=!1,D.value=null;try{const[e,s,i]=await Promise.allSettled([ot(t.id),at(t.id),rt(t.id)]);if(e.status==="rejected"&&s.status==="rejected"){const P=e.reason;j.value=P?.response?.data?.error?.message||P?.message||"Failed to load invocation detail"}e.status==="fulfilled"&&(n.value={...t,...e.value.data}),s.status==="fulfilled"&&(_.value=s.value.data.stderr||"",U.value=s.value.data.log_entries||[]),i.status==="fulfilled"?d.value=i.value.data:F.value=!0}finally{Y.value=!1}},de=(t,e=3)=>{if(e<=0||typeof t!="string")return t;const s=t.trim();if(!s||!'{["tfn0123456789-'.includes(s[0]))return t;try{const i=JSON.parse(s);return de(i,e-1)}catch{return t}},Le=t=>{if(!t)return"";const e=de(t);if(typeof e=="string")return e;try{return JSON.stringify(e,null,2)}catch{return String(t)}},Pe=()=>{if(!n.value||!d.value)return;const t=E(n.value.function_id);if(!t||t===n.value.function_id?.slice(0,12)){C.notify({title:"Function not loaded",message:"Could not resolve the function name. Try opening the function from the dashboard first, then revisit this drawer.",danger:!0});return}const e={method:d.value.method,path:d.value.path,headers:d.value.headers||{},body:d.value.body||""},s=btoa(unescape(encodeURIComponent(JSON.stringify(e))));X.push({path:`/functions/${t}`,query:{prefill:s}}),O.value=!1},Ne=async()=>{if(!(!n.value||q.value||F.value)){q.value=!0;try{const t=await lt(n.value.id),e=t.headers?.["x-orva-execution-id"]||t.headers?.["X-Orva-Execution-ID"];e?(h(),await L({id:e,function_id:n.value.function_id,status:"running",started_at:new Date().toISOString()})):h()}catch(t){C.notify({title:"Replay failed",message:t?.response?.data?.error?.message||t.message,danger:!0})}finally{q.value=!1}}},Me=async t=>{await ct(t)&&(H.value=!0,setTimeout(()=>H.value=!1,1500))},ze=async()=>{if(!(!n.value||$.value)&&_.value){$.value=!0;try{if(!D.value)try{const e=await ut(n.value.function_id);if(D.value={source:e.data?.code||e.data?.source||"",runtime:e.data?.runtime||""},!D.value.runtime)try{const s=await it(n.value.function_id);D.value.runtime=s.data?.runtime||""}catch{}}catch(e){C.notify({title:"Could not load function source",message:e?.response?.data?.error?.message||e.message||"Source fetch failed. The function may have been deleted.",danger:!0});return}await vt({source:D.value.source,runtime:D.value.runtime,stderr:_.value,requestPreview:d.value?{method:d.value.method,path:d.value.path,headers:d.value.headers||{},body:d.value.body||""}:null,errorMessage:n.value.error_message||"",statusCode:n.value.status_code||""})?C.notify({title:"Prompt copied",message:"Paste into ChatGPT, Claude, or your AI tool of choice."}):C.notify({title:"Copy failed",message:"Could not write to the clipboard. Try again, or copy the stderr by hand.",danger:!0})}finally{$.value=!1}}},ce=()=>{V||(V=setInterval(h,5e3))},ve=()=>{V&&(clearInterval(V),V=null)};return He(async()=>{await $e(),await h(),ce()}),Ge(()=>{h(),ce()}),Je(ve),Ke(ve),(t,e)=>(a(),r("div",ht,[o("div",yt,[e[12]||(e[12]=o("div",null,[o("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Invocation Logs "),o("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"}," Every function execution recorded across HTTP, MCP, cron, and job triggers. Click any row to drill into the captured request, response body, latency breakdown, and handler stderr; replay it against a different version when debugging regressions. ")],-1)),u(R,{variant:"secondary",onClick:ie},{default:S(()=>[u(c(K),{class:z(["w-4 h-4 mr-2",{"animate-spin":b.value}])},null,8,["class"]),e[11]||(e[11]=y(" Refresh ",-1))]),_:1})]),o("div",_t,[o("div",wt,[u(c(Xe),{class:"w-3.5 h-3.5 absolute left-2.5 top-1/2 -translate-y-1/2 text-foreground-muted/60 pointer-events-none"}),fe(o("input",{"onUpdate:modelValue":e[0]||(e[0]=s=>p.value.q=s),placeholder:"Search errors, container ids…","aria-label":"Search invocations by error or container id",class:"w-full bg-background border border-border rounded-md pl-8 pr-3 py-1.5 text-xs text-foreground placeholder-foreground-muted/60 focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onInput:Se},null,544),[[Ze,p.value.q]])]),u(c(le),{options:we,modelValue:p.value.status,label:"Status","onUpdate:modelValue":e[1]||(e[1]=s=>{p.value.status=s,oe()})},null,8,["modelValue"]),u(c(le),{options:ke,modelValue:p.value.range,label:"Range","onUpdate:modelValue":e[2]||(e[2]=s=>{p.value.range=s,oe()})},null,8,["modelValue"]),fe(o("select",{"onUpdate:modelValue":e[3]||(e[3]=s=>p.value.fnId=s),class:"bg-background border border-border rounded-md pl-2.5 pr-2 py-1.5 text-xs text-foreground-muted hover:text-white focus:outline-none focus:border-white max-w-[180px]",onChange:oe},[e[13]||(e[13]=o("option",{value:""}," All functions ",-1)),(a(!0),r(N,null,M(Q.value,(s,i)=>(a(),r("option",{key:i,value:i},l(s),9,kt))),128))],544),[[Ye,p.value.fnId]]),te.value?(a(),r("button",{key:0,class:"text-[11px] text-foreground-muted hover:text-white px-2 py-1.5 transition-colors",onClick:Ce}," Clear ")):v("",!0)]),B.value&&x.value.length===0?(a(),r("div",Ct,[u(c(xe),{class:"w-8 h-8 mx-auto text-danger-fg opacity-60"}),e[15]||(e[15]=o("p",{class:"mt-3 text-sm text-foreground"},"Could not load invocations.",-1)),o("p",St,l(B.value),1),u(R,{variant:"secondary",size:"sm",class:"mt-4",onClick:ie},{default:S(()=>[u(c(K),{class:"w-3.5 h-3.5 mr-2"}),e[14]||(e[14]=y(" Retry ",-1))]),_:1})])):(a(),r("div",Tt,[o("ul",Dt,[(a(!0),r(N,null,M(x.value,s=>(a(),r("li",{key:s.id,class:z(["px-4 py-3 cursor-pointer transition-colors",m.value.has(s.id)?"bg-surface/30":"hover:bg-surface/50"]),onClick:i=>L(s)},[o("div",It,[o("input",{checked:m.value.has(s.id),type:"checkbox",class:"mt-0.5 w-3.5 h-3.5 rounded border-border bg-background shrink-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background",onClick:e[4]||(e[4]=J(()=>{},["stop"])),onChange:i=>ne(s.id)},null,40,Rt),o("div",qt,[o("div",$t,[o("span",Vt,l(E(s.function_id)),1),u(re,{status:s.status},null,8,["status"])]),o("div",Et,[o("span",null,l(G(s.started_at)),1),s.duration_ms!=null?(a(),r("span",Lt,l(s.duration_ms)+"ms",1)):v("",!0),s.status_code!=null?(a(),r("span",Pt,"HTTP "+l(s.status_code),1)):v("",!0),s.cold_start?(a(),r("span",Nt,"cold")):v("",!0)]),s.trace_id?(a(),r("div",Mt," trace "+l(s.trace_id.substring(0,11)),1)):v("",!0)])])],10,Ft))),128)),x.value.length===0&&!b.value?(a(),r("li",zt,l(te.value?"No matches.":"No invocations yet."),1)):v("",!0)]),o("table",At,[o("thead",Bt,[o("tr",null,[o("th",Ot,[o("input",{ref_key:"selectAllRef",ref:ee,type:"checkbox",checked:W.value,"aria-label":"Select all invocations",class:"w-3.5 h-3.5 rounded border-border bg-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background",onChange:_e},null,40,jt)]),e[16]||(e[16]=o("th",{class:"px-4 py-3 font-medium"},"Time",-1)),e[17]||(e[17]=o("th",{class:"px-4 py-3 font-medium"},"Function",-1)),e[18]||(e[18]=o("th",{class:"px-4 py-3 font-medium"},"Status",-1)),e[19]||(e[19]=o("th",{class:"px-4 py-3 font-medium hidden md:table-cell"},"Cold",-1)),e[20]||(e[20]=o("th",{class:"px-4 py-3 font-medium hidden lg:table-cell"},"HTTP",-1)),e[21]||(e[21]=o("th",{class:"px-4 py-3 font-medium hidden sm:table-cell"},"Duration",-1)),e[22]||(e[22]=o("th",{class:"px-4 py-3 font-medium hidden lg:table-cell"},"Trace",-1)),e[23]||(e[23]=o("th",{class:"px-4 py-3 font-medium text-right hidden xl:table-cell"},"ID",-1))])]),o("tbody",Ut,[(a(!0),r(N,null,M(x.value,s=>(a(),r("tr",{key:s.id,class:z(["hover:bg-surface/50 transition-colors cursor-pointer",{"bg-surface/30":m.value.has(s.id)}]),onClick:i=>L(s)},[o("td",{class:"px-4 py-3 align-middle",onClick:e[5]||(e[5]=J(()=>{},["stop"]))},[o("input",{checked:m.value.has(s.id),type:"checkbox","aria-label":"Select invocation",class:"w-3.5 h-3.5 rounded border-border bg-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background",onChange:i=>ne(s.id)},null,40,Gt)]),o("td",Jt,l(G(s.started_at)),1),o("td",Kt,[o("span",{class:"hover:underline",onClick:J(i=>c(X).push("/functions/"+E(s.function_id)),["stop"])},l(E(s.function_id)),9,Xt)]),o("td",Zt,[u(re,{status:s.status},null,8,["status"])]),o("td",Yt,[s.cold_start?(a(),r("span",Qt," cold ")):(a(),r("span",Wt,l(c(T)),1))]),o("td",es,l(s.status_code??c(T)),1),o("td",ts,l(s.duration_ms!=null?s.duration_ms+"ms":c(T)),1),o("td",ss,[s.trace_id?(a(),r("button",{key:0,class:"text-foreground-muted hover:text-white font-mono text-xs underline-offset-2 hover:underline inline-flex items-center gap-1",title:s.trace_id,onClick:J(i=>c(X).push("/traces/"+s.trace_id),["stop"])},[u(c(st),{class:"w-3 h-3"}),y(" "+l(s.trace_id.substring(0,11)),1)],8,os)):(a(),r("span",as,l(c(T)),1))]),o("td",rs,l(s.id?.substring(0,12)),1)],10,Ht))),128)),x.value.length===0&&!b.value?(a(),r("tr",ns,[o("td",ls,l(te.value?"No matches.":"No invocations yet."),1)])):v("",!0)])]),he.value?(a(),r("div",us,[o("button",{class:"text-xs text-foreground-muted hover:text-white transition-colors flex items-center gap-1.5",disabled:b.value,onClick:Ve},[b.value?(a(),pe(c(K),{key:0,class:"w-3 h-3 animate-spin"})):v("",!0),y(" "+l(b.value?"Loading…":`Load more (${A.value-x.value.length} more)`),1)],8,is)])):v("",!0)])),u(Qe,{name:"fade"},{default:S(()=>[m.value.size?(a(),r("div",ds,[o("span",cs,l(m.value.size)+" selected",1),e[24]||(e[24]=o("span",{class:"w-px h-4 bg-border"},null,-1)),o("button",{class:"text-xs text-foreground-muted hover:text-white transition-colors px-2 py-1",onClick:e[6]||(e[6]=s=>m.value=new Set)}," Clear "),u(R,{variant:"danger",size:"sm",class:"!rounded-full px-4",loading:Z.value,onClick:Ee},{default:S(()=>[u(c(mt),{class:"w-3.5 h-3.5"}),y(" Delete "+l(m.value.size),1)]),_:1},8,["loading"])])):v("",!0)]),_:1}),u(dt,{modelValue:O.value,"onUpdate:modelValue":e[10]||(e[10]=s=>O.value=s),title:De.value,width:"640px"},{default:S(()=>[Y.value?(a(),r("div",vs," Loading… ")):j.value?(a(),r("div",fs,[u(c(xe),{class:"w-8 h-8 mx-auto text-danger-fg opacity-60"}),e[26]||(e[26]=o("p",{class:"mt-3 text-sm text-foreground"},"Could not load this invocation.",-1)),o("p",ps,l(j.value),1),u(R,{variant:"secondary",size:"sm",class:"mt-4",onClick:e[7]||(e[7]=s=>ae.value&&L(ae.value))},{default:S(()=>[u(c(K),{class:"w-3.5 h-3.5 mr-2"}),e[25]||(e[25]=y(" Retry ",-1))]),_:1})])):n.value?(a(),r("div",xs,[o("div",gs,[u(re,{status:n.value.status},null,8,["status"]),n.value.cold_start?(a(),r("span",bs," cold start ")):v("",!0),n.value.status_code?(a(),r("span",hs," HTTP "+l(n.value.status_code),1)):v("",!0),n.value.replay_of?(a(),r("span",{key:2,class:"inline-flex items-center gap-1 px-2.5 py-1 rounded text-xs border bg-background font-mono text-foreground-muted hover:text-white cursor-pointer",title:"Open the original execution",onClick:e[8]||(e[8]=s=>L({id:n.value.replay_of,function_id:n.value.function_id,status:"success",started_at:n.value.started_at}))},[u(c(xt),{class:"w-3 h-3"}),y(" replay of "+l(n.value.replay_of?.substring(0,14))+"… ",1)])):v("",!0)]),o("div",ys,[u(I,{label:"Duration",value:n.value.duration_ms!=null?n.value.duration_ms+" ms":c(T)},null,8,["value"]),u(I,{label:"Response size",value:n.value.response_size!=null?qe(n.value.response_size):c(T)},null,8,["value"]),u(I,{label:"Started",value:G(n.value.started_at)},null,8,["value"]),u(I,{label:"Finished",value:n.value.finished_at?G(n.value.finished_at):c(T)},null,8,["value"]),u(I,{label:"Function",value:E(n.value.function_id)},null,8,["value"]),u(I,{label:"Execution ID",value:n.value.id,mono:""},null,8,["value"])]),n.value.error_message?(a(),r("div",_s,[e[27]||(e[27]=o("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},"Error",-1)),o("pre",ws,l(n.value.error_message),1)])):v("",!0),d.value?(a(),r("div",ks,[e[30]||(e[30]=o("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},"Request",-1)),o("div",Cs,[o("div",Ss,[o("span",Ts,l(d.value.method),1),o("span",Ds,l(d.value.path),1)]),d.value.headers&&Object.keys(d.value.headers).length?(a(),r("div",Fs,[e[28]||(e[28]=o("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Headers",-1)),o("div",Is,[(a(!0),r(N,null,M(d.value.headers,(s,i)=>(a(),r("div",{key:i,class:"font-mono text-[11px] flex gap-2 py-0.5"},[o("span",Rs,l(i)+":",1),o("span",{class:z(["text-foreground break-all",{"text-warning-fg":s==="[REDACTED]"}])},l(s),3)]))),128))])])):v("",!0),d.value.body?(a(),r("div",qs,[e[29]||(e[29]=o("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Body",-1)),o("pre",$s,l(Le(d.value.body)),1)])):v("",!0),d.value.truncated?(a(),r("div",Vs," Body was truncated at the configured cap. Replay is disabled for this row. ")):v("",!0),o("div",{class:"pt-1 flex justify-end"},[o("button",{type:"button",class:"text-[11px] text-foreground-muted hover:text-white px-2 py-1 rounded hover:bg-surface-hover transition-colors",title:"Open the editor with this request prefilled",onClick:Pe}," Save as fixture → ")])])])):v("",!0),U.value.length>0?(a(),r("div",Es,[o("h3",Ls," Logs ("+l(U.value.length)+") ",1),o("div",Ps,[(a(!0),r(N,null,M(U.value,s=>(a(),r("div",{key:`exec-log-${s.id}`,class:"flex items-baseline gap-2"},[o("span",Ns,l(be(s.ts)),1),o("span",{class:z(["text-[10px] uppercase",ge(s.level)])},l(s.level),3),o("span",Ms,l(s.message),1),s.fields?(a(),r("code",zs,l(s.fields),1)):v("",!0)]))),128))])])):v("",!0),o("div",null,[o("div",As,[e[31]||(e[31]=o("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted"},[y(" Stderr "),o("span",{class:"text-[10px] normal-case text-foreground-muted/70"},"(stdout is the response body, not stored)")],-1)),_.value?(a(),r("button",{key:0,class:"text-xs text-foreground-muted hover:text-white",onClick:e[9]||(e[9]=s=>Me(_.value))},l(H.value?"copied!":"copy"),1)):v("",!0)]),o("pre",Bs,l(_.value||"(no stderr captured)"),1)]),o("div",Os,[u(R,{variant:"primary",disabled:F.value||d.value?.truncated||q.value,loading:q.value,title:Fe.value,onClick:Ne},{default:S(()=>[u(c(gt),{class:"w-4 h-4 mr-2"}),e[32]||(e[32]=y(" Replay ",-1))]),_:1},8,["disabled","loading","title"]),Ie.value?(a(),pe(R,{key:0,variant:"secondary",disabled:!_.value||$.value,loading:$.value,title:Re.value,onClick:ze},{default:S(()=>[u(c(bt),{class:"w-4 h-4 mr-2"}),e[33]||(e[33]=y(" Suggest fix ",-1))]),_:1},8,["disabled","loading","title"])):v("",!0),F.value?(a(),r("span",js," Request not captured for this invocation. ")):v("",!0)])])):(a(),r("div",ms," No invocation selected. "))]),_:1},8,["modelValue","title"])]))}};export{io as default}; diff --git a/backend/internal/server/ui_dist/assets/InvocationsLog-DdGp94hr.js b/backend/internal/server/ui_dist/assets/InvocationsLog-DdGp94hr.js deleted file mode 100644 index f1d8742..0000000 --- a/backend/internal/server/ui_dist/assets/InvocationsLog-DdGp94hr.js +++ /dev/null @@ -1,5 +0,0 @@ -import{C as Ne,o as re,E as ze,G as Oe,y as Ae,a as l,b as s,d as i,h as I,_ as M,f as v,S as Ue,e as le,v as je,R as He,F as N,p as z,g as f,t as n,n as ne,k as C,T as Je,H as Ge,ay as ue,r as p,az as Ke,z as _,q as w,i as Xe,j as r,s as O,w as Z,aA as Ye,aB as Ze,aC as Qe,aD as We,J as et,aE as tt,aF as st,ak as at}from"./index-WXhXpu06.js";import{E as S}from"./format-CsU4_SPu.js";import{D as ot}from"./Drawer-DFsQitq5.js";import{_ as de}from"./StatusBadge-B3a6roHm.js";import{c as rt}from"./clipboard-CmSw2rR-.js";import{c as lt}from"./aiPrompts-Dgb3jxRL.js";import{R as ie}from"./refresh-cw-CEunRyai.js";import{C as nt}from"./chevron-down-CtdSqW4P.js";import{C as ut}from"./check-CuO1EVch.js";import{T as dt}from"./trash-2-dFLn8UYZ.js";import{R as it}from"./rotate-ccw-Cx2X0X0k.js";import{P as ct}from"./play-l-CnPJiF.js";import{S as vt}from"./sparkles-Bj1MS2Kp.js";const pt={class:"space-y-6"},ft={class:"flex items-center justify-between"},mt={class:"flex items-center gap-2 flex-wrap"},xt={class:"relative flex-1 min-w-[280px] max-w-[440px]"},gt=["value"],ht={class:"bg-background border border-border rounded-lg overflow-x-auto"},bt={class:"w-full text-sm text-left"},yt={class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},_t={class:"px-4 py-3 w-8"},wt=["checked",".indeterminate"],kt={class:"divide-y divide-border"},Ct=["onClick"],St=["checked","onChange"],Tt={class:"px-4 py-3 text-foreground"},Dt={class:"px-4 py-3 font-medium text-white"},Ft=["onClick"],It={class:"px-4 py-3"},Rt={class:"px-4 py-3 hidden md:table-cell"},qt={key:0,class:"inline-flex items-center px-2 py-0.5 rounded text-xs border bg-background font-mono text-blue-400 border-blue-900/40"},Vt={key:1,class:"text-foreground-muted text-xs"},$t={class:"px-4 py-3 text-foreground-muted font-mono text-xs hidden lg:table-cell"},Et={class:"px-4 py-3 text-foreground-muted font-mono text-xs hidden sm:table-cell"},Pt={class:"px-4 py-3 hidden lg:table-cell"},Lt=["title","onClick"],Bt={key:1,class:"text-foreground-muted"},Mt={class:"px-4 py-3 text-right text-foreground-muted font-mono text-xs hidden xl:table-cell"},Nt={key:0},zt={colspan:"8",class:"px-6 py-8 text-center text-foreground-muted"},Ot={key:0,class:"flex justify-center border-t border-border py-3 bg-surface/30"},At=["disabled"],Ut={key:0,class:"fixed bottom-4 left-1/2 -translate-x-1/2 z-30 flex items-center gap-3 bg-background border border-border shadow-2xl rounded-full pl-4 pr-2 py-2"},jt={class:"text-xs text-white"},Ht={key:0,class:"p-6 text-sm text-foreground-muted"},Jt={key:1,class:"p-6 text-sm text-foreground-muted"},Gt={key:2,class:"p-5 space-y-5"},Kt={class:"flex items-center gap-2 flex-wrap"},Xt={key:0,class:"inline-flex items-center px-2.5 py-1 rounded text-xs border bg-background font-mono text-blue-400 border-blue-900/40"},Yt={key:1,class:"inline-flex items-center px-2.5 py-1 rounded text-xs border bg-background font-mono text-foreground-muted"},Zt={class:"grid grid-cols-2 gap-3 text-sm"},Qt={key:0},Wt={class:"bg-red-950/30 border border-red-900/40 rounded p-3 text-xs text-red-300 font-mono whitespace-pre-wrap break-words"},es={key:1},ts={class:"bg-surface border border-border rounded p-3 space-y-3"},ss={class:"flex items-center gap-2 font-mono text-xs"},as={class:"px-2 py-0.5 rounded bg-background text-white border border-border"},os={class:"text-foreground-muted truncate"},rs={key:0},ls={class:"bg-background border border-border rounded p-2 max-h-40 overflow-auto"},ns={class:"text-foreground-muted shrink-0"},us={key:1},ds={class:"bg-background border border-border rounded p-2 text-xs text-foreground font-mono overflow-auto max-h-40 whitespace-pre-wrap break-words"},is={key:2,class:"text-[11px] text-yellow-500/90"},cs={key:2},vs={class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},ps={class:"bg-surface border border-border rounded p-3 text-xs font-mono space-y-1 max-h-72 overflow-auto"},fs={class:"text-foreground-muted text-[10px] tabular-nums"},ms={class:"text-white truncate"},xs={key:0,class:"text-[10px] text-foreground-muted truncate"},gs={class:"flex items-center justify-between mb-2"},hs={class:"bg-surface border border-border rounded p-3 text-xs text-foreground font-mono overflow-auto max-h-72 whitespace-pre-wrap break-words"},bs={class:"pt-2 border-t border-border flex items-center gap-3"},ys={key:1,class:"text-xs text-foreground-muted"},_s=50,Ls={__name:"InvocationsLog",setup(ws){const k=Ne(),A=Xe(),g=p([]),$=p(0),b=p(!1),U=p(!1),E=p(!1),j=p(!1),o=p(null),m=p(new Set),y=p(""),P=p([]),ce=t=>{switch(t){case"error":return"text-red-300";case"warn":return"text-amber-300";case"debug":return"text-foreground-muted";default:return"text-primary-light"}},ve=t=>{if(!t)return"";try{const e=new Date(t);return e.toLocaleTimeString(void 0,{hour12:!1})+"."+String(e.getMilliseconds()).padStart(3,"0")}catch{return t}},L=p(!1),H=p({}),d=p(null),D=p(!1),R=p(!1),q=p(!1),T=p(null);let V=null;const pe=w(()=>g.value.length<$.value),J=w(()=>g.value.length>0&&g.value.every(t=>m.value.has(t.id))),fe=w(()=>g.value.some(t=>m.value.has(t.id))),me=t=>{const e=new Set(m.value);e.has(t)?e.delete(t):e.add(t),m.value=e},xe=()=>{if(J.value)m.value=new Set;else{const t=new Set(m.value);g.value.forEach(e=>t.add(e.id)),m.value=t}},c=p({fnId:"",status:"",range:"",q:""}),ge=[{value:"",label:"All"},{value:"success",label:"Success"},{value:"error",label:"Error"}],he=[{value:"",label:"All time"},{value:"1h",label:"1h"},{value:"24h",label:"24h"},{value:"7d",label:"7d"}],Q=w(()=>!!(c.value.fnId||c.value.status||c.value.range||c.value.q)),be=()=>{c.value={fnId:"",status:"",range:"",q:""},h()};let G=null;const ye=()=>{G&&clearTimeout(G),G=setTimeout(()=>h(),300)},K=()=>h(),_e=t=>{if(!t)return"";const e={"1h":36e5,"24h":864e5,"7d":7*864e5}[t];return e?new Date(Date.now()-e).toISOString():""},we=w(()=>o.value?`Invocation · ${o.value.id?.substring(0,14)}`:"Invocation"),ke=w(()=>D.value?"request not captured":d.value?.truncated?"body was truncated; replay would be inaccurate":"Re-run this exact request against the current code"),Ce=w(()=>{const t=o.value;return t?typeof t.status_code=="number"&&t.status_code>=500?!0:!!t.error_message:!1}),Se=w(()=>y.value?"Build a paste-ready debug prompt with source + request + stderr":"no stderr to debug from"),F={props:{label:String,value:[String,Number],mono:Boolean},template:` -
-
{{ label }}
-
{{ value }}
-
`},W=Ke({name:"FilterChip",props:{options:{type:Array,required:!0},modelValue:{type:String,default:""},label:{type:String,required:!0}},emits:["update:modelValue"],setup(t,{emit:e}){const a=p(!1),u=w(()=>t.options.find(x=>x.value===t.modelValue&&x.value!=="")),oe=()=>{a.value=!1},Le=x=>{e("update:modelValue",x),oe()},Be=x=>{x.stopPropagation(),e("update:modelValue","")},Me=x=>{x.target.closest(".fc-root")||oe()};return()=>_("div",{class:"fc-root relative",onMouseenter:()=>{document.addEventListener("mousedown",Me)},onMouseleave:()=>{}},[_("button",{class:["inline-flex items-center gap-1.5 rounded-md border h-7 px-2.5 text-xs transition-colors",u.value?"bg-primary text-primary-foreground border-primary":"bg-surface text-foreground-muted border-border hover:text-white hover:border-foreground-muted"],onClick:()=>{a.value=!a.value}},[_("span",{class:"text-[10px] uppercase tracking-wider"},t.label+(u.value?":":"")),u.value?_("span",null,u.value.label):null,u.value?_("span",{class:"opacity-70 hover:opacity-100 -mr-0.5",onClick:Be,title:"Clear"},"✕"):_(nt,{class:"w-3 h-3 opacity-60"})]),a.value?_("div",{class:"absolute z-30 mt-1 left-0 min-w-[140px] bg-background border border-border rounded-md shadow-xl py-1"},t.options.filter(x=>x.value!=="").map(x=>_("button",{key:x.value,class:["w-full text-left px-2.5 py-1.5 text-xs flex items-center gap-2 transition-colors",x.value===t.modelValue?"text-white":"text-foreground-muted hover:text-white hover:bg-surface-hover"],onClick:()=>Le(x.value)},[_(ut,{class:["w-3 h-3",x.value===t.modelValue?"opacity-100":"opacity-0"]}),x.label]))):null])}}),X=t=>t?new Date(t).toLocaleString():S,Te=t=>t<1024?`${t} B`:t<1024*1024?`${(t/1024).toFixed(1)} KB`:`${(t/1024/1024).toFixed(1)} MB`,B=t=>H.value[t]||t?.slice(0,12)||"?",De=async()=>{try{((await Ge()).data.functions||[]).forEach(e=>H.value[e.id]=e.name)}catch{}},ee=t=>{const e={limit:_s,offset:t};return c.value.fnId&&(e.function_id=c.value.fnId),c.value.status&&(e.status=c.value.status),c.value.range&&(e.since=_e(c.value.range)),c.value.q&&(e.q=c.value.q),e},h=async()=>{b.value=!0;try{const t=await ue(ee(0));g.value=t.data.executions||[],$.value=t.data.total??g.value.length}catch(t){console.error("Failed to fetch logs:",t)}finally{b.value=!1}},Fe=async()=>{b.value=!0;try{const t=await ue(ee(g.value.length));g.value=[...g.value,...t.data.executions||[]],$.value=t.data.total??g.value.length}catch(t){console.error("Failed to load more:",t)}finally{b.value=!1}},Ie=()=>h(),Re=async()=>{const t=m.value.size;if(!await k.ask({title:`Delete ${t} ${t===1?"invocation":"invocations"}?`,message:"Removes the rows + their stderr logs. This is irreversible.",confirmLabel:`Delete ${t}`,danger:!0}))return;U.value=!0;const a=[...m.value];try{const u=await et.post("/executions/bulk-delete",{ids:a});m.value=new Set,await h(),u.data.failed&&k.notify({title:"Some deletes failed",message:`${u.data.failed} of ${a.length} could not be deleted.`,danger:!0})}catch(u){k.notify({title:"Bulk delete failed",message:u.response?.data?.error?.message||u.message,danger:!0})}finally{U.value=!1}},Y=async t=>{o.value=t,E.value=!0,j.value=!0,y.value="",L.value=!1,d.value=null,D.value=!1,T.value=null;try{const[e,a,u]=await Promise.allSettled([Ze(t.id),Qe(t.id),We(t.id)]);e.status==="fulfilled"&&(o.value={...t,...e.value.data}),a.status==="fulfilled"&&(y.value=a.value.data.stderr||"",P.value=a.value.data.log_entries||[]),u.status==="fulfilled"?d.value=u.value.data:D.value=!0}finally{j.value=!1}},te=(t,e=3)=>{if(e<=0||typeof t!="string")return t;const a=t.trim();if(!a||!'{["tfn0123456789-'.includes(a[0]))return t;try{const u=JSON.parse(a);return te(u,e-1)}catch{return t}},qe=t=>{if(!t)return"";const e=te(t);if(typeof e=="string")return e;try{return JSON.stringify(e,null,2)}catch{return String(t)}},Ve=()=>{if(!o.value||!d.value)return;const t=B(o.value.function_id);if(!t||t===o.value.function_id?.slice(0,12)){k.notify({title:"Function not loaded",message:"Could not resolve the function name. Try opening the function from the dashboard first, then revisit this drawer.",danger:!0});return}const e={method:d.value.method,path:d.value.path,headers:d.value.headers||{},body:d.value.body||""},a=btoa(unescape(encodeURIComponent(JSON.stringify(e))));A.push({path:`/functions/${t}`,query:{prefill:a}}),E.value=!1},$e=async()=>{if(!(!o.value||R.value||D.value)){R.value=!0;try{const t=await tt(o.value.id),e=t.headers?.["x-orva-execution-id"]||t.headers?.["X-Orva-Execution-ID"];e?(h(),await Y({id:e,function_id:o.value.function_id,status:"running",started_at:new Date().toISOString()})):h()}catch(t){k.notify({title:"Replay failed",message:t?.response?.data?.error?.message||t.message,danger:!0})}finally{R.value=!1}}},Ee=async t=>{await rt(t)&&(L.value=!0,setTimeout(()=>L.value=!1,1500))},Pe=async()=>{if(!(!o.value||q.value)&&y.value){q.value=!0;try{if(!T.value)try{const e=await st(o.value.function_id);if(T.value={source:e.data?.code||e.data?.source||"",runtime:e.data?.runtime||""},!T.value.runtime)try{const a=await at(o.value.function_id);T.value.runtime=a.data?.runtime||""}catch{}}catch(e){k.notify({title:"Could not load function source",message:e?.response?.data?.error?.message||e.message||"Source fetch failed. The function may have been deleted.",danger:!0});return}await lt({source:T.value.source,runtime:T.value.runtime,stderr:y.value,requestPreview:d.value?{method:d.value.method,path:d.value.path,headers:d.value.headers||{},body:d.value.body||""}:null,errorMessage:o.value.error_message||"",statusCode:o.value.status_code||""})?k.notify({title:"Prompt copied",message:"Paste into ChatGPT, Claude, or your AI tool of choice."}):k.notify({title:"Copy failed",message:"Could not write to the clipboard. Try again, or copy the stderr by hand.",danger:!0})}finally{q.value=!1}}};re(async()=>{await De(),await h()});const se=()=>{V||(V=setInterval(h,5e3))},ae=()=>{V&&(clearInterval(V),V=null)};return re(se),ze(()=>{h(),se()}),Oe(ae),Ae(ae),(t,e)=>(r(),l("div",pt,[s("div",ft,[e[10]||(e[10]=s("div",null,[s("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Invocation Logs "),s("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"}," Every function execution recorded across HTTP, MCP, cron, and job triggers. Click any row to drill into the captured request, response body, latency breakdown, and handler stderr; replay it against a different version when debugging regressions. ")],-1)),i(M,{variant:"secondary",onClick:Ie},{default:I(()=>[i(v(ie),{class:O(["w-4 h-4 mr-2",{"animate-spin":b.value}])},null,8,["class"]),e[9]||(e[9]=C(" Refresh ",-1))]),_:1})]),s("div",mt,[s("div",xt,[i(v(Ue),{class:"w-3.5 h-3.5 absolute left-2.5 top-1/2 -translate-y-1/2 text-foreground-muted/60 pointer-events-none"}),le(s("input",{"onUpdate:modelValue":e[0]||(e[0]=a=>c.value.q=a),placeholder:"Search errors, container ids…",class:"w-full bg-background border border-border rounded-md pl-8 pr-3 py-1.5 text-xs text-foreground placeholder-foreground-muted/60 focus:outline-none focus:border-white",onInput:ye},null,544),[[je,c.value.q]])]),i(v(W),{options:ge,modelValue:c.value.status,label:"Status","onUpdate:modelValue":e[1]||(e[1]=a=>{c.value.status=a,K()})},null,8,["modelValue"]),i(v(W),{options:he,modelValue:c.value.range,label:"Range","onUpdate:modelValue":e[2]||(e[2]=a=>{c.value.range=a,K()})},null,8,["modelValue"]),le(s("select",{"onUpdate:modelValue":e[3]||(e[3]=a=>c.value.fnId=a),class:"bg-background border border-border rounded-md pl-2.5 pr-2 py-1.5 text-xs text-foreground-muted hover:text-white focus:outline-none focus:border-white max-w-[180px]",onChange:K},[e[11]||(e[11]=s("option",{value:""}," All functions ",-1)),(r(!0),l(N,null,z(H.value,(a,u)=>(r(),l("option",{key:u,value:u},n(a),9,gt))),128))],544),[[He,c.value.fnId]]),Q.value?(r(),l("button",{key:0,class:"text-[11px] text-foreground-muted hover:text-white px-2 py-1.5 transition-colors",onClick:be}," Clear ")):f("",!0)]),s("div",ht,[s("table",bt,[s("thead",yt,[s("tr",null,[s("th",_t,[s("input",{type:"checkbox",checked:J.value,".indeterminate":fe.value&&!J.value,class:"w-3.5 h-3.5 rounded border-border bg-background",onChange:xe},null,40,wt)]),e[12]||(e[12]=s("th",{class:"px-4 py-3 font-medium"},"Time",-1)),e[13]||(e[13]=s("th",{class:"px-4 py-3 font-medium"},"Function",-1)),e[14]||(e[14]=s("th",{class:"px-4 py-3 font-medium"},"Status",-1)),e[15]||(e[15]=s("th",{class:"px-4 py-3 font-medium hidden md:table-cell"},"Cold",-1)),e[16]||(e[16]=s("th",{class:"px-4 py-3 font-medium hidden lg:table-cell"},"HTTP",-1)),e[17]||(e[17]=s("th",{class:"px-4 py-3 font-medium hidden sm:table-cell"},"Duration",-1)),e[18]||(e[18]=s("th",{class:"px-4 py-3 font-medium hidden lg:table-cell"},"Trace",-1)),e[19]||(e[19]=s("th",{class:"px-4 py-3 font-medium text-right hidden xl:table-cell"},"ID",-1))])]),s("tbody",kt,[(r(!0),l(N,null,z(g.value,a=>(r(),l("tr",{key:a.id,class:O(["hover:bg-surface/50 transition-colors cursor-pointer",{"bg-surface/30":m.value.has(a.id)}]),onClick:u=>Y(a)},[s("td",{class:"px-4 py-3 align-middle",onClick:e[4]||(e[4]=Z(()=>{},["stop"]))},[s("input",{checked:m.value.has(a.id),type:"checkbox",class:"w-3.5 h-3.5 rounded border-border bg-background",onChange:u=>me(a.id)},null,40,St)]),s("td",Tt,n(X(a.started_at)),1),s("td",Dt,[s("span",{class:"hover:underline",onClick:Z(u=>v(A).push("/functions/"+B(a.function_id)),["stop"])},n(B(a.function_id)),9,Ft)]),s("td",It,[i(de,{status:a.status},null,8,["status"])]),s("td",Rt,[a.cold_start?(r(),l("span",qt," cold ")):(r(),l("span",Vt,n(v(S)),1))]),s("td",$t,n(a.status_code??v(S)),1),s("td",Et,n(a.duration_ms!=null?a.duration_ms+"ms":v(S)),1),s("td",Pt,[a.trace_id?(r(),l("button",{key:0,class:"text-foreground-muted hover:text-white font-mono text-xs underline-offset-2 hover:underline inline-flex items-center gap-1",title:a.trace_id,onClick:Z(u=>v(A).push("/traces/"+a.trace_id),["stop"])},[i(v(Ye),{class:"w-3 h-3"}),C(" "+n(a.trace_id.substring(0,11)),1)],8,Lt)):(r(),l("span",Bt,n(v(S)),1))]),s("td",Mt,n(a.id?.substring(0,12)),1)],10,Ct))),128)),g.value.length===0&&!b.value?(r(),l("tr",Nt,[s("td",zt,n(Q.value?"No matches.":"No invocations yet."),1)])):f("",!0)])]),pe.value?(r(),l("div",Ot,[s("button",{class:"text-xs text-foreground-muted hover:text-white transition-colors flex items-center gap-1.5",disabled:b.value,onClick:Fe},[b.value?(r(),ne(v(ie),{key:0,class:"w-3 h-3 animate-spin"})):f("",!0),C(" "+n(b.value?"Loading…":`Load more (${$.value-g.value.length} more)`),1)],8,At)])):f("",!0)]),i(Je,{name:"fade"},{default:I(()=>[m.value.size?(r(),l("div",Ut,[s("span",jt,n(m.value.size)+" selected",1),e[20]||(e[20]=s("span",{class:"w-px h-4 bg-border"},null,-1)),s("button",{class:"text-xs text-foreground-muted hover:text-white transition-colors px-2 py-1",onClick:e[5]||(e[5]=a=>m.value=new Set)}," Clear "),i(M,{variant:"danger",size:"sm",class:"!rounded-full px-4",loading:U.value,onClick:Re},{default:I(()=>[i(v(dt),{class:"w-3.5 h-3.5"}),C(" Delete "+n(m.value.size),1)]),_:1},8,["loading"])])):f("",!0)]),_:1}),i(ot,{modelValue:E.value,"onUpdate:modelValue":e[8]||(e[8]=a=>E.value=a),title:we.value,width:"640px"},{default:I(()=>[j.value?(r(),l("div",Ht," Loading… ")):o.value?(r(),l("div",Gt,[s("div",Kt,[i(de,{status:o.value.status},null,8,["status"]),o.value.cold_start?(r(),l("span",Xt," cold start ")):f("",!0),o.value.status_code?(r(),l("span",Yt," HTTP "+n(o.value.status_code),1)):f("",!0),o.value.replay_of?(r(),l("span",{key:2,class:"inline-flex items-center gap-1 px-2.5 py-1 rounded text-xs border bg-background font-mono text-foreground-muted hover:text-white cursor-pointer",title:"Open the original execution",onClick:e[6]||(e[6]=a=>Y({id:o.value.replay_of,function_id:o.value.function_id,status:"success",started_at:o.value.started_at}))},[i(v(it),{class:"w-3 h-3"}),C(" replay of "+n(o.value.replay_of?.substring(0,14))+"… ",1)])):f("",!0)]),s("div",Zt,[i(F,{label:"Duration",value:o.value.duration_ms!=null?o.value.duration_ms+" ms":v(S)},null,8,["value"]),i(F,{label:"Response size",value:o.value.response_size!=null?Te(o.value.response_size):v(S)},null,8,["value"]),i(F,{label:"Started",value:X(o.value.started_at)},null,8,["value"]),i(F,{label:"Finished",value:o.value.finished_at?X(o.value.finished_at):v(S)},null,8,["value"]),i(F,{label:"Function",value:B(o.value.function_id)},null,8,["value"]),i(F,{label:"Execution ID",value:o.value.id,mono:""},null,8,["value"])]),o.value.error_message?(r(),l("div",Qt,[e[21]||(e[21]=s("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},"Error",-1)),s("pre",Wt,n(o.value.error_message),1)])):f("",!0),d.value?(r(),l("div",es,[e[24]||(e[24]=s("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted mb-2"},"Request",-1)),s("div",ts,[s("div",ss,[s("span",as,n(d.value.method),1),s("span",os,n(d.value.path),1)]),d.value.headers&&Object.keys(d.value.headers).length?(r(),l("div",rs,[e[22]||(e[22]=s("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Headers",-1)),s("div",ls,[(r(!0),l(N,null,z(d.value.headers,(a,u)=>(r(),l("div",{key:u,class:"font-mono text-[11px] flex gap-2 py-0.5"},[s("span",ns,n(u)+":",1),s("span",{class:O(["text-foreground break-all",{"text-yellow-500/90":a==="[REDACTED]"}])},n(a),3)]))),128))])])):f("",!0),d.value.body?(r(),l("div",us,[e[23]||(e[23]=s("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Body",-1)),s("pre",ds,n(qe(d.value.body)),1)])):f("",!0),d.value.truncated?(r(),l("div",is," Body was truncated at the configured cap. Replay is disabled for this row. ")):f("",!0),s("div",{class:"pt-1 flex justify-end"},[s("button",{type:"button",class:"text-[11px] text-foreground-muted hover:text-white px-2 py-1 rounded hover:bg-surface-hover transition-colors",title:"Open the editor with this request prefilled",onClick:Ve}," Save as fixture → ")])])])):f("",!0),P.value.length>0?(r(),l("div",cs,[s("h3",vs," Logs ("+n(P.value.length)+") ",1),s("div",ps,[(r(!0),l(N,null,z(P.value,a=>(r(),l("div",{key:`exec-log-${a.id}`,class:"flex items-baseline gap-2"},[s("span",fs,n(ve(a.ts)),1),s("span",{class:O(["text-[10px] uppercase",ce(a.level)])},n(a.level),3),s("span",ms,n(a.message),1),a.fields?(r(),l("code",xs,n(a.fields),1)):f("",!0)]))),128))])])):f("",!0),s("div",null,[s("div",gs,[e[25]||(e[25]=s("h3",{class:"text-xs uppercase tracking-wider text-foreground-muted"},[C(" Stderr "),s("span",{class:"text-[10px] normal-case text-foreground-muted/70"},"(stdout is the response body, not stored)")],-1)),y.value?(r(),l("button",{key:0,class:"text-xs text-foreground-muted hover:text-white",onClick:e[7]||(e[7]=a=>Ee(y.value))},n(L.value?"copied!":"copy"),1)):f("",!0)]),s("pre",hs,n(y.value||"(no stderr captured)"),1)]),s("div",bs,[i(M,{variant:"primary",disabled:D.value||d.value?.truncated||R.value,loading:R.value,title:ke.value,onClick:$e},{default:I(()=>[i(v(ct),{class:"w-4 h-4 mr-2"}),e[26]||(e[26]=C(" Replay ",-1))]),_:1},8,["disabled","loading","title"]),Ce.value?(r(),ne(M,{key:0,variant:"secondary",disabled:!y.value||q.value,loading:q.value,title:Se.value,onClick:Pe},{default:I(()=>[i(v(vt),{class:"w-4 h-4 mr-2"}),e[27]||(e[27]=C(" Suggest fix ",-1))]),_:1},8,["disabled","loading","title"])):f("",!0),D.value?(r(),l("span",ys," Request not captured for this invocation. ")):f("",!0)])])):(r(),l("div",Jt," Nothing drawerRow. "))]),_:1},8,["modelValue","title"])]))}};export{Ls as default}; diff --git a/backend/internal/server/ui_dist/assets/Jobs-DZdX38G3.js b/backend/internal/server/ui_dist/assets/Jobs-DZdX38G3.js deleted file mode 100644 index 6072497..0000000 --- a/backend/internal/server/ui_dist/assets/Jobs-DZdX38G3.js +++ /dev/null @@ -1 +0,0 @@ -import{c as V,C as z,o as B,M as O,a as n,b as e,k as d,t as l,F as w,p as k,d as i,h as u,_ as b,f as p,g as x,at as T,q as _,ah as U,r as C,j as r,P as H,e as v,R as K,v as J,a4 as W,s as Y,n as G,H as Q,au as X,av as Z,aw as j}from"./index-WXhXpu06.js";import{E as L}from"./format-CsU4_SPu.js";import{_ as M}from"./IconButton-CclydhPm.js";import{D as ee}from"./Drawer-DFsQitq5.js";import{R as te}from"./rotate-ccw-Cx2X0X0k.js";import{T as se}from"./trash-2-dFLn8UYZ.js";const ae=V("inbox",[["polyline",{points:"22 12 16 12 14 15 10 15 8 12 2 12",key:"o97t9d"}],["path",{d:"M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z",key:"oot6mr"}]]);const oe=V("refresh-ccw",[["path",{d:"M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"14sxne"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}],["path",{d:"M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16",key:"1hlbsb"}],["path",{d:"M16 16h5v5",key:"ccwih5"}]]),le={class:"space-y-4"},re={class:"flex items-start justify-between gap-4 flex-wrap"},ne={class:"flex items-center gap-2 text-xs text-foreground-muted"},de={class:"flex items-center gap-2"},ie={class:"flex items-center gap-2 sm:flex-wrap overflow-x-auto sm:overflow-visible scrollable snap-x min-w-0 flex-1"},ue={key:0,class:"ml-1 opacity-70 tabular-nums"},ce={class:"flex items-center gap-2 shrink-0"},fe={class:"p-5 space-y-5 text-sm"},me=["value"],pe={class:"flex items-center gap-2 text-xs text-foreground-muted"},xe={key:0},he={key:1,class:"text-xs text-error"},be={class:"flex items-center justify-end gap-2"},ve={class:"bg-background border border-border rounded-lg overflow-x-auto"},ge={class:"w-full text-sm text-left"},ye={class:"divide-y divide-border"},we={class:"px-4 py-3 font-medium text-white"},ke={class:"flex flex-col"},_e={class:"text-[10px] text-foreground-muted font-mono"},Ce={class:"px-4 py-3 hidden sm:table-cell"},Se=["title"],qe={class:"px-4 py-3 text-foreground-muted text-xs hidden md:table-cell"},De={class:"px-4 py-3 text-foreground-muted text-xs hidden lg:table-cell"},Ie={class:"px-4 py-3 text-foreground-muted text-xs hidden xl:table-cell"},Je={class:"px-4 py-3 text-right"},Le={class:"inline-flex items-center gap-1"},Me={key:0},Ve={colspan:"6",class:"px-4 py-12 text-center"},Ae={class:"text-foreground-muted text-sm"},Be={__name:"Jobs",setup(Ee){const g=z(),c=C([]),h=C([]),f=C("all");let y=null;const a=U({open:!1,fnId:"",payload:"{}",scheduleLater:!1,scheduledAt:"",saving:!1,error:""}),A=[{value:"all",label:"All"},{value:"pending",label:"Pending"},{value:"running",label:"Running"},{value:"succeeded",label:"Succeeded"},{value:"failed",label:"Failed"}],E=_(()=>c.value.length),S=_(()=>f.value==="all"?c.value:c.value.filter(o=>o.status===f.value)),q=_(()=>{const o={all:c.value.length};for(const s of c.value)o[s.status]=(o[s.status]||0)+1;return o}),F=o=>{switch(o){case"pending":return"bg-amber-500/10 text-amber-300 border-amber-500/30";case"running":return"bg-sky-500/15 text-sky-300 border-sky-500/30 animate-pulse";case"succeeded":return"bg-success/10 text-success border-success/30";case"failed":return"bg-error/10 text-error border-error/30";default:return"bg-surface text-foreground-muted border-border"}},D=o=>o?new Date(o).toLocaleString("en-US",{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit"}):L,m=async()=>{try{const o=await T({limit:200});c.value=o.data.jobs||[]}catch(o){console.error("Failed to load jobs",o)}},N=async o=>{try{await j(o.id),await m()}catch(s){g.notify({title:"Retry failed",message:s.message,danger:!0})}},P=async o=>{if(await g.ask({title:"Delete job?",message:`Job ${o.id} will be removed. This cannot be undone.`,confirmLabel:"Delete",danger:!0}))try{await Z(o.id),await m()}catch(t){g.notify({title:"Delete failed",message:t.message,danger:!0})}},R=async()=>{if(a.error="",a.payload="{}",a.scheduleLater=!1,a.scheduledAt="",h.value.length===0)try{const o=await Q();h.value=o.data?.functions||[],h.value.length&&!a.fnId&&(a.fnId=h.value[0].id)}catch(o){console.error("list functions failed",o)}a.open=!0},$=async()=>{a.error="";let o;try{o=a.payload.trim()?JSON.parse(a.payload):{}}catch(t){a.error="Payload must be valid JSON: "+t.message;return}const s={function_id:a.fnId,payload:o};if(a.scheduleLater){if(!a.scheduledAt){a.error='Pick a date/time, or untick "Schedule for later".';return}s.scheduled_at=new Date(a.scheduledAt).toISOString()}a.saving=!0;try{await X(s),a.open=!1,await m()}catch(t){a.error=t?.response?.data?.error?.message||t.message||"Enqueue failed"}finally{a.saving=!1}};return B(()=>{m(),y=setInterval(m,5e3)}),O(()=>{y&&clearInterval(y)}),(o,s)=>(r(),n("div",le,[e("div",re,[s[6]||(s[6]=e("div",null,[e("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Jobs "),e("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"},[d(" Background work queued via "),e("code",{class:"font-mono text-[11px]"},"jobs.enqueue()"),d(" from the SDK or the "),e("code",{class:"font-mono text-[11px]"},"enqueue_job"),d(" MCP tool. Workers pick them up at the configured concurrency, retry on failure with exponential backoff, and surface here with their full lifecycle. ")])],-1)),e("div",ne," Background queue · "+l(E.value)+" jobs ",1)]),e("div",de,[e("div",ie,[(r(),n(w,null,k(A,t=>i(b,{key:t.value,variant:"chip",size:"xs",active:f.value===t.value,class:"shrink-0 snap-start",onClick:I=>f.value=t.value},{default:u(()=>[d(l(t.label)+" ",1),q.value[t.value]!==void 0?(r(),n("span",ue,l(q.value[t.value]),1)):x("",!0)]),_:2},1032,["active","onClick"])),64))]),e("div",ce,[i(b,{size:"xs",onClick:R},{default:u(()=>[i(p(H),{class:"w-3 h-3"}),s[7]||(s[7]=d(" Enqueue ",-1))]),_:1}),i(b,{variant:"secondary",size:"xs",onClick:m},{default:u(()=>[i(p(oe),{class:"w-3 h-3"}),s[8]||(s[8]=d(" Refresh ",-1))]),_:1})])]),i(ee,{modelValue:a.open,"onUpdate:modelValue":s[5]||(s[5]=t=>a.open=t),title:"Enqueue a job",width:"560px"},{footer:u(()=>[e("div",be,[i(b,{variant:"ghost",size:"sm",onClick:s[4]||(s[4]=t=>a.open=!1)},{default:u(()=>[...s[14]||(s[14]=[d(" Cancel ",-1)])]),_:1}),i(b,{size:"sm",disabled:!a.fnId||a.saving,loading:a.saving,onClick:$},{default:u(()=>[...s[15]||(s[15]=[d(" Enqueue ",-1)])]),_:1},8,["disabled","loading"])])]),default:u(()=>[e("div",fe,[e("div",null,[s[9]||(s[9]=e("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"},"Function",-1)),v(e("select",{"onUpdate:modelValue":s[0]||(s[0]=t=>a.fnId=t),class:"mt-2 w-full bg-surface border border-border rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-white"},[(r(!0),n(w,null,k(h.value,t=>(r(),n("option",{key:t.id,value:t.id},l(t.name)+" ("+l(t.runtime)+") ",9,me))),128))],512),[[K,a.fnId]])]),e("div",null,[s[10]||(s[10]=e("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"},"Payload (JSON)",-1)),v(e("textarea",{"onUpdate:modelValue":s[1]||(s[1]=t=>a.payload=t),rows:"6",spellcheck:"false",class:"mt-2 w-full bg-surface border border-border rounded p-3 text-xs text-white font-mono focus:outline-none focus:border-white",placeholder:'{"hello":"world"}'},null,512),[[J,a.payload]])]),e("div",null,[e("label",pe,[v(e("input",{"onUpdate:modelValue":s[2]||(s[2]=t=>a.scheduleLater=t),type:"checkbox"},null,512),[[W,a.scheduleLater]]),s[11]||(s[11]=d(" Schedule for later ",-1))]),s[12]||(s[12]=e("p",{class:"text-[11px] text-foreground-muted mt-1"}," Off: runs on the next scheduler tick (~5s). On: holds until the timestamp below. ",-1))]),a.scheduleLater?(r(),n("div",xe,[s[13]||(s[13]=e("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"},"Run at (local time)",-1)),v(e("input",{"onUpdate:modelValue":s[3]||(s[3]=t=>a.scheduledAt=t),type:"datetime-local",class:"mt-2 w-full bg-surface border border-border rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-white"},null,512),[[J,a.scheduledAt]])])):x("",!0),a.error?(r(),n("div",he,l(a.error),1)):x("",!0)])]),_:1},8,["modelValue"]),e("div",ve,[e("table",ge,[s[17]||(s[17]=e("thead",{class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},[e("tr",null,[e("th",{class:"px-4 py-3 font-medium"}," Function "),e("th",{class:"px-4 py-3 font-medium hidden sm:table-cell"}," Status "),e("th",{class:"px-4 py-3 font-medium hidden md:table-cell"}," Attempts "),e("th",{class:"px-4 py-3 font-medium hidden lg:table-cell"}," Scheduled "),e("th",{class:"px-4 py-3 font-medium hidden xl:table-cell"}," Finished "),e("th",{class:"px-4 py-3 font-medium text-right"}," Actions ")])],-1)),e("tbody",ye,[(r(!0),n(w,null,k(S.value,t=>(r(),n("tr",{key:t.id,class:"hover:bg-surface/50 transition-colors"},[e("td",we,[e("div",ke,[e("span",null,l(t.function_name||t.function_id),1),e("span",_e,l(t.id),1)])]),e("td",Ce,[e("span",{class:Y(["inline-flex items-center px-2 py-0.5 rounded text-[11px] font-medium border",F(t.status)])},l(t.status),3),t.last_error?(r(),n("p",{key:0,class:"text-[11px] text-error mt-1 truncate max-w-xs",title:t.last_error},l(t.last_error),9,Se)):x("",!0)]),e("td",qe,l(t.attempts)+" / "+l(t.max_attempts),1),e("td",De,l(D(t.scheduled_at)),1),e("td",Ie,l(t.finished_at?D(t.finished_at):p(L)),1),e("td",Je,[e("div",Le,[t.status==="failed"?(r(),G(M,{key:0,icon:p(te),variant:"success",title:"Retry",onClick:I=>N(t)},null,8,["icon","onClick"])):x("",!0),i(M,{icon:p(se),variant:"danger",title:"Delete",onClick:I=>P(t)},null,8,["icon","onClick"])])])]))),128)),S.value.length===0?(r(),n("tr",Me,[e("td",Ve,[i(p(ae),{class:"w-10 h-10 text-foreground-muted mx-auto mb-3 opacity-30"}),e("p",Ae,l(f.value==="all"?"No jobs yet.":`No ${f.value} jobs.`),1),s[16]||(s[16]=e("p",{class:"text-foreground-muted text-xs mt-1"},[d(" Enqueue from inside a function with "),e("code",{class:"font-mono text-[11px] px-1.5 py-0.5 rounded bg-surface border border-border"},"orva.jobs.enqueue(name, payload)"),d(". ")],-1))])])):x("",!0)])])])]))}};export{Be as default}; diff --git a/backend/internal/server/ui_dist/assets/Jobs-DlXhx955.js b/backend/internal/server/ui_dist/assets/Jobs-DlXhx955.js new file mode 100644 index 0000000..8786a85 --- /dev/null +++ b/backend/internal/server/ui_dist/assets/Jobs-DlXhx955.js @@ -0,0 +1 @@ +import{c as O,G as H,o as W,J as X,a as r,b as e,k as d,t as l,F as w,p as C,d as i,h as m,_ as v,f as u,g as c,az as Y,q as A,ao as Q,r as I,j as o,P as Z,e as q,V as j,v as N,ad as ee,s as $,n as S,K as F,a0 as te,aA as se,aB as ae,aC as ne}from"./index-CmY58qNN.js";import{E as L}from"./format-CsU4_SPu.js";import{_ as D}from"./IconButton-i2ilFBbf.js";import{D as le}from"./Drawer-B7DqDJXO.js";import{R as P}from"./rotate-ccw-DQrtkmfg.js";import{T as R}from"./trash-2-BJzRpJ7Y.js";import{C as oe,a as re}from"./circle-DeGmlwna.js";import{C as de}from"./circle-check-Z_CRCWca.js";import{C as z}from"./clock-CbnKorJ0.js";const B=O("inbox",[["polyline",{points:"22 12 16 12 14 15 10 15 8 12 2 12",key:"o97t9d"}],["path",{d:"M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z",key:"oot6mr"}]]);const ie=O("refresh-ccw",[["path",{d:"M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"14sxne"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}],["path",{d:"M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16",key:"1hlbsb"}],["path",{d:"M16 16h5v5",key:"ccwih5"}]]),ue={class:"space-y-4"},ce={class:"flex items-start justify-between gap-4 flex-wrap"},fe={class:"flex items-center gap-2 text-xs text-foreground-muted"},me={class:"flex items-center gap-2"},pe={class:"flex items-center gap-2 sm:flex-wrap overflow-x-auto sm:overflow-visible scrollable snap-x min-w-0 flex-1"},xe={key:0,class:"ml-1 opacity-70 tabular-nums"},ge={class:"flex items-center gap-2 shrink-0"},he={class:"p-5 space-y-5 text-sm"},ve=["value"],ye={class:"flex items-center gap-2 text-xs text-foreground-muted"},be={key:0},_e={key:1,class:"text-xs text-danger-fg"},ke={class:"flex items-center justify-end gap-2"},we={class:"bg-background border border-border rounded-lg overflow-x-auto"},Ce={class:"sm:hidden divide-y divide-border"},qe={class:"flex items-start justify-between gap-2"},Se={class:"min-w-0 flex-1"},De={class:"font-medium text-white truncate"},Je={class:"text-[10px] text-foreground-muted font-mono break-all"},Ae={class:"mt-2"},Ie=["title"],Le={class:"mt-2 grid grid-cols-2 gap-x-3 gap-y-1 text-[11px] text-foreground-muted"},Ve={class:"col-span-2"},Ee={class:"flex items-center gap-1 shrink-0"},Me={key:0,class:"px-4 py-12 text-center"},Ne={class:"text-foreground-muted text-sm"},$e={class:"hidden sm:table w-full text-sm text-left"},Fe={class:"divide-y divide-border"},Pe={class:"px-4 py-3 font-medium text-white"},Re={class:"flex flex-col"},ze={class:"text-[10px] text-foreground-muted font-mono"},Be={class:"px-4 py-3"},Oe=["title"],Te={class:"px-4 py-3 text-foreground-muted text-xs hidden md:table-cell"},Ue={class:"px-4 py-3 text-foreground-muted text-xs hidden lg:table-cell"},Ke={class:"px-4 py-3 text-foreground-muted text-xs hidden xl:table-cell"},Ge={class:"px-4 py-3 text-right"},He={class:"inline-flex items-center gap-1"},We={key:0},Xe={colspan:"6",class:"px-4 py-12 text-center"},Ye={class:"text-foreground-muted text-sm"},rt={__name:"Jobs",setup(Qe){const J=H(),p=I([]),g=I([]),f=I("all");let h=null;const a=Q({open:!1,fnId:"",payload:"{}",scheduleLater:!1,scheduledAt:"",saving:!1,error:""}),T=[{value:"all",label:"All"},{value:"pending",label:"Pending"},{value:"running",label:"Running"},{value:"succeeded",label:"Succeeded"},{value:"failed",label:"Failed"}],U=A(()=>p.value.length),y=A(()=>f.value==="all"?p.value:p.value.filter(n=>n.status===f.value)),V=A(()=>{const n={all:p.value.length};for(const s of p.value)n[s.status]=(n[s.status]||0)+1;return n}),b=n=>{switch(n){case"pending":return{classes:"bg-warning-tint text-warning-fg border-warning-ring",icon:z};case"running":return{classes:"bg-info-tint text-info-fg border-info-ring",icon:z};case"succeeded":return{classes:"bg-success-tint text-success-fg border-success-ring",icon:de};case"failed":return{classes:"bg-danger-tint text-danger-fg border-danger-ring",icon:re};default:return{classes:"bg-surface text-foreground-muted border-border",icon:oe}}},_=n=>n?new Date(n).toLocaleString("en-US",{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit"}):L,x=async()=>{try{const n=await Y({limit:200});p.value=n.data.jobs||[]}catch(n){console.error("Failed to load jobs",n)}},E=async n=>{try{await ne(n.id),await x()}catch(s){J.notify({title:"Retry failed",message:s.message,danger:!0})}},M=async n=>{if(await J.ask({title:"Delete job?",message:`Job ${n.id} will be removed. This cannot be undone.`,confirmLabel:"Delete",danger:!0}))try{await ae(n.id),await x()}catch(t){J.notify({title:"Delete failed",message:t.message,danger:!0})}},K=async()=>{if(a.error="",a.payload="{}",a.scheduleLater=!1,a.scheduledAt="",g.value.length===0)try{const n=await te();g.value=n.data?.functions||[],g.value.length&&!a.fnId&&(a.fnId=g.value[0].id)}catch(n){console.error("list functions failed",n)}a.open=!0},G=async()=>{a.error="";let n;try{n=a.payload.trim()?JSON.parse(a.payload):{}}catch(t){a.error="Payload must be valid JSON: "+t.message;return}const s={function_id:a.fnId,payload:n};if(a.scheduleLater){if(!a.scheduledAt){a.error='Pick a date/time, or untick "Schedule for later".';return}s.scheduled_at=new Date(a.scheduledAt).toISOString()}a.saving=!0;try{await se(s),a.open=!1,await x()}catch(t){a.error=t?.response?.data?.error?.message||t.message||"Enqueue failed"}finally{a.saving=!1}};return W(()=>{x(),h||(h=setInterval(x,5e3))}),X(()=>{h&&(clearInterval(h),h=null)}),(n,s)=>(o(),r("div",ue,[e("div",ce,[s[6]||(s[6]=e("div",null,[e("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Jobs "),e("p",{class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"},[d(" Background work queued via "),e("code",{class:"font-mono text-[11px]"},"jobs.enqueue()"),d(" from the SDK or the "),e("code",{class:"font-mono text-[11px]"},"enqueue_job"),d(" MCP tool. Workers pick them up at the configured concurrency, retry on failure with exponential backoff, and surface here with their full lifecycle. ")])],-1)),e("div",fe," Background queue · "+l(U.value)+" jobs ",1)]),e("div",me,[e("div",pe,[(o(),r(w,null,C(T,t=>i(v,{key:t.value,variant:"chip",size:"xs",active:f.value===t.value,class:"shrink-0 snap-start",onClick:k=>f.value=t.value},{default:m(()=>[d(l(t.label)+" ",1),V.value[t.value]!==void 0?(o(),r("span",xe,l(V.value[t.value]),1)):c("",!0)]),_:2},1032,["active","onClick"])),64))]),e("div",ge,[i(v,{size:"xs",onClick:K},{default:m(()=>[i(u(Z),{class:"w-3 h-3"}),s[7]||(s[7]=d(" Enqueue ",-1))]),_:1}),i(v,{variant:"secondary",size:"xs",onClick:x},{default:m(()=>[i(u(ie),{class:"w-3 h-3"}),s[8]||(s[8]=d(" Refresh ",-1))]),_:1})])]),i(le,{modelValue:a.open,"onUpdate:modelValue":s[5]||(s[5]=t=>a.open=t),title:"Enqueue a job",width:"560px"},{footer:m(()=>[e("div",ke,[i(v,{variant:"ghost",size:"sm",onClick:s[4]||(s[4]=t=>a.open=!1)},{default:m(()=>[...s[14]||(s[14]=[d(" Cancel ",-1)])]),_:1}),i(v,{size:"sm",disabled:!a.fnId||a.saving,loading:a.saving,onClick:G},{default:m(()=>[...s[15]||(s[15]=[d(" Enqueue ",-1)])]),_:1},8,["disabled","loading"])])]),default:m(()=>[e("div",he,[e("div",null,[s[9]||(s[9]=e("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"},"Function",-1)),q(e("select",{"onUpdate:modelValue":s[0]||(s[0]=t=>a.fnId=t),class:"mt-2 w-full bg-surface border border-border rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-white"},[(o(!0),r(w,null,C(g.value,t=>(o(),r("option",{key:t.id,value:t.id},l(t.name)+" ("+l(t.runtime)+") ",9,ve))),128))],512),[[j,a.fnId]])]),e("div",null,[s[10]||(s[10]=e("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"},"Payload (JSON)",-1)),q(e("textarea",{"onUpdate:modelValue":s[1]||(s[1]=t=>a.payload=t),rows:"6",spellcheck:"false",class:"mt-2 w-full bg-surface border border-border rounded p-3 text-xs text-white font-mono focus:outline-none focus:border-white",placeholder:'{"hello":"world"}'},null,512),[[N,a.payload]])]),e("div",null,[e("label",ye,[q(e("input",{"onUpdate:modelValue":s[2]||(s[2]=t=>a.scheduleLater=t),type:"checkbox"},null,512),[[ee,a.scheduleLater]]),s[11]||(s[11]=d(" Schedule for later ",-1))]),s[12]||(s[12]=e("p",{class:"text-[11px] text-foreground-muted mt-1"}," Off: runs on the next scheduler tick (~5s). On: holds until the timestamp below. ",-1))]),a.scheduleLater?(o(),r("div",be,[s[13]||(s[13]=e("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"},"Run at (local time)",-1)),q(e("input",{"onUpdate:modelValue":s[3]||(s[3]=t=>a.scheduledAt=t),type:"datetime-local",class:"mt-2 w-full bg-surface border border-border rounded px-3 py-2 text-sm text-white focus:outline-none focus:border-white"},null,512),[[N,a.scheduledAt]])])):c("",!0),a.error?(o(),r("div",_e,l(a.error),1)):c("",!0)])]),_:1},8,["modelValue"]),e("div",we,[e("ul",Ce,[(o(!0),r(w,null,C(y.value,t=>(o(),r("li",{key:t.id,class:"px-4 py-3"},[e("div",qe,[e("div",Se,[e("div",De,l(t.function_name||t.function_id),1),e("div",Je,l(t.id),1),e("div",Ae,[e("span",{class:$(["inline-flex items-center gap-1 px-2 py-0.5 rounded text-[11px] font-medium border",b(t.status).classes])},[(o(),S(F(b(t.status).icon),{class:"w-3 h-3 shrink-0","aria-hidden":"true"})),d(" "+l(t.status),1)],2)]),t.last_error?(o(),r("p",{key:0,class:"text-[11px] text-danger-fg mt-1 break-all",title:t.last_error},l(t.last_error),9,Ie)):c("",!0),e("dl",Le,[e("div",null,[s[16]||(s[16]=e("dt",{class:"uppercase tracking-wider text-[10px]"}," Attempts ",-1)),e("dd",null,l(t.attempts)+" / "+l(t.max_attempts),1)]),e("div",null,[s[17]||(s[17]=e("dt",{class:"uppercase tracking-wider text-[10px]"}," Scheduled ",-1)),e("dd",null,l(_(t.scheduled_at)),1)]),e("div",Ve,[s[18]||(s[18]=e("dt",{class:"uppercase tracking-wider text-[10px]"}," Finished ",-1)),e("dd",null,l(t.finished_at?_(t.finished_at):u(L)),1)])])]),e("div",Ee,[t.status==="failed"?(o(),S(D,{key:0,icon:u(P),variant:"success",title:"Retry",onClick:k=>E(t)},null,8,["icon","onClick"])):c("",!0),i(D,{icon:u(R),variant:"danger",title:"Delete",onClick:k=>M(t)},null,8,["icon","onClick"])])])]))),128)),y.value.length===0?(o(),r("li",Me,[i(u(B),{class:"w-10 h-10 text-foreground-muted mx-auto mb-3 opacity-60"}),e("p",Ne,l(f.value==="all"?"No jobs yet.":`No ${f.value} jobs.`),1),s[19]||(s[19]=e("p",{class:"text-foreground-muted text-xs mt-1"},[d(" Enqueue from inside a function with "),e("code",{class:"font-mono text-[11px] px-1.5 py-0.5 rounded bg-surface border border-border"},"orva.jobs.enqueue(name, payload)"),d(". ")],-1))])):c("",!0)]),e("table",$e,[s[21]||(s[21]=e("thead",{class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},[e("tr",null,[e("th",{class:"px-4 py-3 font-medium"}," Function "),e("th",{class:"px-4 py-3 font-medium"}," Status "),e("th",{class:"px-4 py-3 font-medium hidden md:table-cell"}," Attempts "),e("th",{class:"px-4 py-3 font-medium hidden lg:table-cell"}," Scheduled "),e("th",{class:"px-4 py-3 font-medium hidden xl:table-cell"}," Finished "),e("th",{class:"px-4 py-3 font-medium text-right"}," Actions ")])],-1)),e("tbody",Fe,[(o(!0),r(w,null,C(y.value,t=>(o(),r("tr",{key:t.id,class:"hover:bg-surface/50 transition-colors"},[e("td",Pe,[e("div",Re,[e("span",null,l(t.function_name||t.function_id),1),e("span",ze,l(t.id),1)])]),e("td",Be,[e("span",{class:$(["inline-flex items-center gap-1 px-2 py-0.5 rounded text-[11px] font-medium border",b(t.status).classes])},[(o(),S(F(b(t.status).icon),{class:"w-3 h-3 shrink-0","aria-hidden":"true"})),d(" "+l(t.status),1)],2),t.last_error?(o(),r("p",{key:0,class:"text-[11px] text-danger-fg mt-1 truncate max-w-xs",title:t.last_error},l(t.last_error),9,Oe)):c("",!0)]),e("td",Te,l(t.attempts)+" / "+l(t.max_attempts),1),e("td",Ue,l(_(t.scheduled_at)),1),e("td",Ke,l(t.finished_at?_(t.finished_at):u(L)),1),e("td",Ge,[e("div",He,[t.status==="failed"?(o(),S(D,{key:0,icon:u(P),variant:"success",title:"Retry",onClick:k=>E(t)},null,8,["icon","onClick"])):c("",!0),i(D,{icon:u(R),variant:"danger",title:"Delete",onClick:k=>M(t)},null,8,["icon","onClick"])])])]))),128)),y.value.length===0?(o(),r("tr",We,[e("td",Xe,[i(u(B),{class:"w-10 h-10 text-foreground-muted mx-auto mb-3 opacity-60"}),e("p",Ye,l(f.value==="all"?"No jobs yet.":`No ${f.value} jobs.`),1),s[20]||(s[20]=e("p",{class:"text-foreground-muted text-xs mt-1"},[d(" Enqueue from inside a function with "),e("code",{class:"font-mono text-[11px] px-1.5 py-0.5 rounded bg-surface border border-border"},"orva.jobs.enqueue(name, payload)"),d(". ")],-1))])])):c("",!0)])])])]))}};export{rt as default}; diff --git a/backend/internal/server/ui_dist/assets/KVStore-B5D0R9Ta.js b/backend/internal/server/ui_dist/assets/KVStore-B5D0R9Ta.js new file mode 100644 index 0000000..f89b65d --- /dev/null +++ b/backend/internal/server/ui_dist/assets/KVStore-B5D0R9Ta.js @@ -0,0 +1 @@ +import{G as le,o as ae,X as ie,Z as de,a as i,b as t,k as u,d as f,h as p,t as a,g as v,_ as b,f as w,$ as ue,e as h,v as _,F as S,p as q,an as fe,q as Y,a5 as ce,r as T,ao as A,ab as pe,j as d,s as C,P as xe,w as X,ap as G,aq as me}from"./index-CmY58qNN.js";import{E as N}from"./format-CsU4_SPu.js";import{_ as Z}from"./IconButton-i2ilFBbf.js";import{D as H}from"./Drawer-B7DqDJXO.js";import{R as ve}from"./refresh-cw-63KivPtM.js";import{T as U}from"./trash-2-BJzRpJ7Y.js";const ye={class:"space-y-6"},ge={class:"flex items-start justify-between gap-4"},be={class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"},we={class:"flex items-center gap-2"},ke={class:"text-xs text-foreground-muted"},he={key:0},_e={class:"flex items-center gap-2 flex-wrap"},Se={class:"relative flex-1 min-w-[260px] max-w-[420px]"},Te={key:1,class:"text-[11px] text-amber-400/80"},Ce={class:"bg-background border border-border rounded-lg overflow-x-auto"},De={class:"sm:hidden divide-y divide-border"},Ne=["onClick"],Ve={class:"flex items-start justify-between gap-2"},Me={class:"min-w-0 flex-1"},ze={class:"font-mono text-xs text-white break-all"},Le={class:"mt-1 font-mono text-[11px] text-foreground-muted break-all"},$e={class:"mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-[11px] text-foreground-muted"},Ke={class:"font-mono"},Oe={key:0,class:"px-6 py-12 text-center text-sm text-foreground-muted"},Je={class:"bg-surface px-1.5 py-0.5 rounded text-xs font-mono"},Ue={class:"hidden sm:table w-full text-sm text-left"},Fe={class:"divide-y divide-border"},je=["onClick"],Ie={class:"px-4 py-3 font-mono text-xs text-white truncate max-w-[360px]"},Pe={class:"px-4 py-3 font-mono text-xs text-foreground-muted truncate max-w-[420px] hidden md:table-cell"},Be={class:"px-4 py-3 hidden sm:table-cell"},Re={key:1,class:"text-foreground-muted text-xs"},Ee={class:"px-4 py-3 text-xs font-mono text-foreground-muted hidden lg:table-cell"},qe={class:"px-4 py-3 text-xs text-foreground-muted hidden md:table-cell"},Ye={key:0},Ae={colspan:"6",class:"px-4 py-12 text-center text-foreground-muted text-sm"},Xe={class:"bg-surface px-1.5 py-0.5 rounded text-xs font-mono"},Ge={key:0,class:"p-5 space-y-5 text-sm"},Ze={class:"grid grid-cols-2 gap-3"},He={class:"bg-surface border border-border rounded p-3 min-w-0"},Qe={class:"text-xs text-white font-mono break-all"},We={class:"bg-surface border border-border rounded p-3 min-w-0"},et={class:"bg-surface border border-border rounded p-3 min-w-0"},tt={class:"text-xs text-white font-mono truncate"},st={class:"bg-surface border border-border rounded p-3 min-w-0"},ot={class:"text-xs text-white font-mono"},rt={class:"flex items-center justify-between mb-2"},nt={key:0,class:"text-xs text-red-400"},lt={class:"flex items-center justify-between"},at={class:"flex items-center gap-2"},it={class:"p-5 space-y-5 text-sm"},dt={class:"flex items-center justify-between mb-2"},ut={key:0,class:"text-xs text-red-400"},ft={class:"flex items-center justify-end gap-2"},ct=31536e3,wt={__name:"KVStore",setup(pt){const Q=pe(),V=le(),k=Y(()=>Q.params.name),y=T([]),L=T(0),F=T(!1),D=T(!1),m=T(!1),x=T(""),r=A({open:!1,row:null,text:"",ttlSeconds:0,error:""}),n=A({open:!1,key:"",text:"",ttlSeconds:0,error:""}),M=s=>{const e=Number(s);return!Number.isFinite(e)||e<0||e>ct},j=Y(()=>y.value.reduce((s,e)=>s+(e.size_bytes||0),0)),g=async()=>{D.value=!0;try{const s={limit:200};x.value&&(s.prefix=x.value);const e=await fe(k.value,s);y.value=e.data?.entries||[],L.value=e.data?.total??y.value.length,F.value=!!e.data?.truncated}catch(s){console.error("kvList failed",s),V.notify({title:"Failed to load KV",message:s?.response?.data?.error?.message||"Unknown error",danger:!0})}finally{D.value=!1}};let $=null;const W=()=>{clearTimeout($),$=setTimeout(g,250)},I=s=>{r.row=s,r.text=re(s.value),r.ttlSeconds=s.expires_at?Math.max(0,Math.floor((new Date(s.expires_at)-Date.now())/1e3)):0,r.error="",r.open=!0},ee=async()=>{if(M(r.ttlSeconds)){r.error="TTL must be between 0 and 31536000 (1 year).";return}let s;try{s=JSON.parse(r.text)}catch(e){r.error="Invalid JSON: "+e.message;return}r.error="",m.value=!0;try{await G(k.value,r.row.key,{value:s,ttl_seconds:r.ttlSeconds||0}),r.open=!1,await g()}catch(e){r.error=e?.response?.data?.error?.message||"Save failed"}finally{m.value=!1}},te=async()=>{!r.row||!await V.ask({title:"Delete key?",message:`"${r.row.key}" will be removed from this function's KV store. This cannot be undone.`,confirmLabel:"Delete",danger:!0})||(await B(r.row.key),r.open=!1)},se=()=>{n.key="",n.text="",n.ttlSeconds=0,n.error="",n.open=!0},oe=async()=>{const s=n.key.trim();if(!s){n.error="Key is required";return}if(M(n.ttlSeconds)){n.error="TTL must be between 0 and 31536000 (1 year).";return}let e=n.text.trim()||'""',l;try{l=JSON.parse(e)}catch(o){n.error="Invalid JSON: "+o.message;return}n.error="",m.value=!0;try{await G(k.value,s,{value:l,ttl_seconds:n.ttlSeconds||0}),n.open=!1,await g()}catch(o){n.error=o?.response?.data?.error?.message||"Save failed"}finally{m.value=!1}},P=async s=>{await V.ask({title:"Delete key?",message:`"${s.key}" will be removed from this function's KV store.`,confirmLabel:"Delete",danger:!0})&&await B(s.key)},B=async s=>{try{await me(k.value,s),await g()}catch(e){V.notify({title:"Failed to delete key",message:e?.response?.data?.error?.message||"Unknown error",danger:!0})}},K=(s,e=3)=>{if(e<=0||typeof s!="string")return s;const l=s.trim();if(!l||!'{["tfn0123456789-'.includes(l[0]))return s;try{const o=JSON.parse(l);return K(o,e-1)}catch{return s}},re=s=>{const e=K(s);try{return JSON.stringify(e,null,2)}catch{return String(s)}},R=s=>{if(s==null)return N;const e=K(s);if(typeof e=="string"){const l=JSON.stringify(e);return l.length>80?l.slice(0,80)+"…":l}try{const l=JSON.stringify(e);return l.length>80?l.slice(0,80)+"…":l}catch{return String(e)}},z=s=>s==null?N:s<1024?s+" B":s<1024*1024?(s/1024).toFixed(1)+" KB":(s/1024/1024).toFixed(1)+" MB",E=s=>{if(!s)return N;const e=Date.now()-new Date(s).getTime();if(e<0)return"just now";const l=Math.floor(e/1e3);if(l<60)return l+"s ago";const o=Math.floor(l/60);if(o<60)return o+"m ago";const c=Math.floor(o/60);return c<24?c+"h ago":Math.floor(c/24)+"d ago"},ne=s=>s?new Date(s).toLocaleString():N,O=s=>{const e=new Date(s).getTime()-Date.now();if(e<=0)return"expired";const l=Math.floor(e/1e3);if(l<60)return"in "+l+"s";const o=Math.floor(l/60);if(o<60)return"in "+o+"m";const c=Math.floor(o/60);return c<24?"in "+c+"h "+o%60+"m":"in "+Math.floor(c/24)+"d "+c%24+"h"},J=s=>{if(!s)return"text-foreground-muted";const e=new Date(s).getTime()-Date.now();return e<=6e4?"text-red-400":e<=5*6e4?"text-amber-400":"text-foreground-muted"};return ae(g),ie(g),de(()=>{clearTimeout($)}),(s,e)=>{const l=ce("router-link");return d(),i("div",ye,[t("div",ge,[t("div",null,[e[16]||(e[16]=t("h1",{class:"text-xl font-semibold text-white tracking-tight"}," KV Store ",-1)),t("p",be,[e[14]||(e[14]=u(" Per-function key/value state for ",-1)),f(l,{to:`/functions/${k.value}`,class:"text-white underline"},{default:p(()=>[u(a(k.value),1)]),_:1},8,["to"]),e[15]||(e[15]=u(" Values are JSON, optional TTL. ",-1))])]),t("div",we,[t("span",ke,[u(a(L.value)+" "+a(L.value===1?"key":"keys")+" ",1),j.value>0?(d(),i("span",he,"· "+a(z(j.value)),1)):v("",!0)]),f(b,{variant:"secondary",size:"sm",onClick:g},{default:p(()=>[f(w(ve),{class:C(["w-3.5 h-3.5",{"animate-spin":D.value}])},null,8,["class"]),e[17]||(e[17]=u(" Refresh ",-1))]),_:1}),f(b,{size:"sm",onClick:e[0]||(e[0]=o=>se())},{default:p(()=>[f(w(xe),{class:"w-3.5 h-3.5"}),e[18]||(e[18]=u(" Set key ",-1))]),_:1})])]),t("div",_e,[t("div",Se,[f(w(ue),{class:"w-3.5 h-3.5 absolute left-2.5 top-1/2 -translate-y-1/2 text-foreground-muted/60 pointer-events-none"}),h(t("input",{"onUpdate:modelValue":e[1]||(e[1]=o=>x.value=o),"aria-label":"Search keys by prefix",placeholder:"Search by key prefix… (e.g. user:)",class:"w-full bg-background border border-border rounded-md pl-8 pr-3 py-1.5 text-xs text-foreground placeholder-foreground-muted/60 focus:outline-none focus:ring-1 focus:ring-white focus:border-white",onInput:W},null,544),[[_,x.value]])]),x.value?(d(),i("button",{key:0,class:"text-[11px] text-foreground-muted hover:text-white px-2 py-1.5 transition-colors",onClick:e[2]||(e[2]=o=>{x.value="",g()})}," Clear ")):v("",!0),F.value?(d(),i("span",Te," Showing first "+a(y.value.length)+". Narrow the prefix to see more. ",1)):v("",!0)]),t("div",Ce,[t("ul",De,[(d(!0),i(S,null,q(y.value,o=>(d(),i("li",{key:o.key,class:"px-4 py-3 cursor-pointer hover:bg-surface-hover transition-colors",onClick:c=>I(o)},[t("div",Ve,[t("div",Me,[t("div",ze,a(o.key),1),t("div",Le,a(R(o.value)),1),t("div",$e,[o.expires_at?(d(),i("span",{key:0,class:C(J(o.expires_at))},a(O(o.expires_at)),3)):v("",!0),t("span",Ke,a(z(o.size_bytes)),1),t("span",null,a(E(o.updated_at)),1)])]),t("div",{class:"shrink-0",onClick:e[3]||(e[3]=X(()=>{},["stop"]))},[f(Z,{icon:w(U),variant:"danger",title:"Delete key",onClick:c=>P(o)},null,8,["icon","onClick"])])])],8,Ne))),128)),!D.value&&!y.value.length?(d(),i("li",Oe,[x.value?(d(),i(S,{key:0},[e[19]||(e[19]=u(" No keys match ",-1)),t("code",Je,a(x.value),1),e[20]||(e[20]=u(". ",-1))],64)):(d(),i(S,{key:1},[e[21]||(e[21]=u(" No keys yet. Your function will write here when it calls ",-1)),e[22]||(e[22]=t("code",{class:"bg-surface px-1.5 py-0.5 rounded text-xs font-mono"},"orva.kv.put(...)",-1)),e[23]||(e[23]=u(". ",-1))],64))])):v("",!0)]),t("table",Ue,[e[29]||(e[29]=t("thead",{class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},[t("tr",null,[t("th",{class:"px-4 py-3"},"Key"),t("th",{class:"px-4 py-3 hidden md:table-cell"},"Value preview"),t("th",{class:"px-4 py-3 w-28 hidden sm:table-cell"},"TTL"),t("th",{class:"px-4 py-3 w-20 hidden lg:table-cell"},"Size"),t("th",{class:"px-4 py-3 w-28 hidden md:table-cell"},"Updated"),t("th",{class:"px-4 py-3 w-10 text-right"})])],-1)),t("tbody",Fe,[(d(!0),i(S,null,q(y.value,o=>(d(),i("tr",{key:o.key,class:"hover:bg-surface-hover cursor-pointer transition-colors",onClick:c=>I(o)},[t("td",Ie,a(o.key),1),t("td",Pe,a(R(o.value)),1),t("td",Be,[o.expires_at?(d(),i("span",{key:0,class:C(["text-xs",J(o.expires_at)])},a(O(o.expires_at)),3)):(d(),i("span",Re,a(w(N)),1))]),t("td",Ee,a(z(o.size_bytes)),1),t("td",qe,a(E(o.updated_at)),1),t("td",{class:"px-4 py-3 text-right",onClick:e[4]||(e[4]=X(()=>{},["stop"]))},[f(Z,{icon:w(U),variant:"danger",title:"Delete key",onClick:c=>P(o)},null,8,["icon","onClick"])])],8,je))),128)),!D.value&&!y.value.length?(d(),i("tr",Ye,[t("td",Ae,[x.value?(d(),i(S,{key:0},[e[24]||(e[24]=u(" No keys match ",-1)),t("code",Xe,a(x.value),1),e[25]||(e[25]=u(". ",-1))],64)):(d(),i(S,{key:1},[e[26]||(e[26]=u(" No keys yet. Your function will write here when it calls ",-1)),e[27]||(e[27]=t("code",{class:"bg-surface px-1.5 py-0.5 rounded text-xs font-mono"},"orva.kv.put(...)",-1)),e[28]||(e[28]=u(". ",-1))],64))])])):v("",!0)])])]),f(H,{modelValue:r.open,"onUpdate:modelValue":e[8]||(e[8]=o=>r.open=o),title:r.row?r.row.key:"Inspect key",width:"640px"},{footer:p(()=>[t("div",lt,[f(b,{variant:"danger",size:"sm",disabled:m.value,onClick:te},{default:p(()=>[f(w(U),{class:"w-3.5 h-3.5"}),e[36]||(e[36]=u(" Delete ",-1))]),_:1},8,["disabled"]),t("div",at,[f(b,{variant:"ghost",size:"sm",onClick:e[7]||(e[7]=o=>r.open=!1)},{default:p(()=>[...e[37]||(e[37]=[u(" Cancel ",-1)])]),_:1}),f(b,{size:"sm",disabled:m.value,loading:m.value,onClick:ee},{default:p(()=>[...e[38]||(e[38]=[u(" Save ",-1)])]),_:1},8,["disabled","loading"])])])]),default:p(()=>[r.row?(d(),i("div",Ge,[t("div",Ze,[t("div",He,[e[30]||(e[30]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Key",-1)),t("div",Qe,a(r.row.key),1)]),t("div",We,[e[31]||(e[31]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"TTL",-1)),t("div",{class:C(["text-xs text-white font-mono",J(r.row.expires_at)])},a(r.row.expires_at?O(r.row.expires_at):"Never"),3)]),t("div",et,[e[32]||(e[32]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Updated",-1)),t("div",tt,a(ne(r.row.updated_at)),1)]),t("div",st,[e[33]||(e[33]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Size",-1)),t("div",ot,a(z(r.row.size_bytes)),1)])]),t("div",null,[e[34]||(e[34]=t("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"}," TTL seconds (0 = never) ",-1)),h(t("input",{"onUpdate:modelValue":e[5]||(e[5]=o=>r.ttlSeconds=o),type:"number",min:"0",max:"31536000",class:"mt-2 w-full bg-surface border border-border rounded px-3 py-2 text-sm text-white font-mono focus:outline-none focus:ring-1 focus:ring-white focus:border-white"},null,512),[[_,r.ttlSeconds,void 0,{number:!0}]]),t("p",{class:C(["text-[11px] mt-1.5",M(r.ttlSeconds)?"text-danger-fg":"text-foreground-muted"])}," Must be between 0 and 31536000 (1 year). ",2)]),t("div",null,[t("div",rt,[e[35]||(e[35]=t("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"},"Value (JSON)",-1)),r.error?(d(),i("span",nt,a(r.error),1)):v("",!0)]),h(t("textarea",{"onUpdate:modelValue":e[6]||(e[6]=o=>r.text=o),rows:"14",spellcheck:"false",class:"w-full bg-surface border border-border rounded p-3 text-xs text-white font-mono leading-relaxed focus:outline-none focus:border-white whitespace-pre overflow-x-auto"},null,512),[[_,r.text]])])])):v("",!0)]),_:1},8,["modelValue","title"]),f(H,{modelValue:n.open,"onUpdate:modelValue":e[13]||(e[13]=o=>n.open=o),title:"Set key",width:"640px"},{footer:p(()=>[t("div",ft,[f(b,{variant:"ghost",size:"sm",onClick:e[12]||(e[12]=o=>n.open=!1)},{default:p(()=>[...e[42]||(e[42]=[u(" Cancel ",-1)])]),_:1}),f(b,{size:"sm",disabled:m.value||!n.key.trim(),loading:m.value,onClick:oe},{default:p(()=>[...e[43]||(e[43]=[u(" Save ",-1)])]),_:1},8,["disabled","loading"])])]),default:p(()=>[t("div",it,[t("div",null,[e[39]||(e[39]=t("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"},"Key",-1)),h(t("input",{"onUpdate:modelValue":e[9]||(e[9]=o=>n.key=o),placeholder:"e.g. user:42",class:"mt-2 w-full bg-surface border border-border rounded px-3 py-2 text-sm text-white font-mono focus:outline-none focus:border-white",spellcheck:"false"},null,512),[[_,n.key]])]),t("div",null,[e[40]||(e[40]=t("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"}," TTL seconds (0 = never) ",-1)),h(t("input",{"onUpdate:modelValue":e[10]||(e[10]=o=>n.ttlSeconds=o),type:"number",min:"0",max:"31536000",class:"mt-2 w-full bg-surface border border-border rounded px-3 py-2 text-sm text-white font-mono focus:outline-none focus:ring-1 focus:ring-white focus:border-white"},null,512),[[_,n.ttlSeconds,void 0,{number:!0}]]),t("p",{class:C(["text-[11px] mt-1.5",M(n.ttlSeconds)?"text-danger-fg":"text-foreground-muted"])}," Must be between 0 and 31536000 (1 year). ",2)]),t("div",null,[t("div",dt,[e[41]||(e[41]=t("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"},"Value (JSON)",-1)),n.error?(d(),i("span",ut,a(n.error),1)):v("",!0)]),h(t("textarea",{"onUpdate:modelValue":e[11]||(e[11]=o=>n.text=o),rows:"14",spellcheck:"false",placeholder:'{"hello": "world"}',class:"w-full bg-surface border border-border rounded p-3 text-xs text-white font-mono leading-relaxed focus:outline-none focus:border-white whitespace-pre overflow-x-auto"},null,512),[[_,n.text]])])])]),_:1},8,["modelValue"])])}}};export{wt as default}; diff --git a/backend/internal/server/ui_dist/assets/KVStore-DvTxtoJb.js b/backend/internal/server/ui_dist/assets/KVStore-DvTxtoJb.js deleted file mode 100644 index 93c1464..0000000 --- a/backend/internal/server/ui_dist/assets/KVStore-DvTxtoJb.js +++ /dev/null @@ -1 +0,0 @@ -import{C as se,o as re,E as oe,G as ne,a as u,b as t,k as i,d,h as p,t as l,g,_ as y,f as k,S as ae,e as h,v as _,F as K,p as le,ag as ie,q as F,U as de,r as S,ah as P,a1 as ue,j as f,s as U,P as fe,w as pe,ai as B,aj as ce}from"./index-WXhXpu06.js";import{E as T}from"./format-CsU4_SPu.js";import{_ as xe}from"./IconButton-CclydhPm.js";import{D as I}from"./Drawer-DFsQitq5.js";import{R as me}from"./refresh-cw-CEunRyai.js";import{T as R}from"./trash-2-dFLn8UYZ.js";const ve={class:"space-y-6"},ge={class:"flex items-start justify-between gap-4"},ye={class:"text-sm text-foreground-muted mt-1.5 max-w-prose leading-body"},be={class:"flex items-center gap-2"},we={class:"text-xs text-foreground-muted"},ke={key:0},he={class:"flex items-center gap-2 flex-wrap"},_e={class:"relative flex-1 min-w-[260px] max-w-[420px]"},Se={key:1,class:"text-[11px] text-amber-400/80"},Te={class:"bg-background border border-border rounded-lg overflow-x-auto"},Ve={class:"w-full text-sm text-left"},Ce={class:"divide-y divide-border"},De=["onClick"],Ne={class:"px-4 py-3 font-mono text-xs text-white truncate max-w-[360px]"},Me={class:"px-4 py-3 font-mono text-xs text-foreground-muted truncate max-w-[420px] hidden md:table-cell"},ze={class:"px-4 py-3 hidden sm:table-cell"},Ke={key:1,class:"text-foreground-muted text-xs"},Ue={class:"px-4 py-3 text-xs font-mono text-foreground-muted hidden lg:table-cell"},Je={class:"px-4 py-3 text-xs text-foreground-muted hidden md:table-cell"},Le={key:0},Oe={colspan:"6",class:"px-4 py-12 text-center text-foreground-muted text-sm"},$e={class:"bg-surface px-1.5 py-0.5 rounded text-xs font-mono"},je={key:0,class:"p-5 space-y-5 text-sm"},Fe={class:"grid grid-cols-2 gap-3"},Pe={class:"bg-surface border border-border rounded p-3 min-w-0"},Be={class:"text-xs text-white font-mono break-all"},Ie={class:"bg-surface border border-border rounded p-3 min-w-0"},Re={class:"bg-surface border border-border rounded p-3 min-w-0"},Ee={class:"text-xs text-white font-mono truncate"},qe={class:"bg-surface border border-border rounded p-3 min-w-0"},Ye={class:"text-xs text-white font-mono"},Ae={class:"flex items-center justify-between mb-2"},Ge={key:0,class:"text-xs text-red-400"},He={class:"flex items-center justify-between"},Qe={class:"flex items-center gap-2"},We={class:"p-5 space-y-5 text-sm"},Xe={class:"flex items-center justify-between mb-2"},Ze={key:0,class:"text-xs text-red-400"},et={class:"flex items-center justify-end gap-2"},it={__name:"KVStore",setup(tt){const E=ue(),V=se(),b=F(()=>E.params.name),w=S([]),D=S(0),J=S(!1),C=S(!1),x=S(!1),m=S(""),o=P({open:!1,row:null,text:"",ttlSeconds:0,error:""}),n=P({open:!1,key:"",text:"",ttlSeconds:0,error:""}),L=F(()=>w.value.reduce((s,e)=>s+(e.size_bytes||0),0)),v=async()=>{C.value=!0;try{const s={limit:200};m.value&&(s.prefix=m.value);const e=await ie(b.value,s);w.value=e.data?.entries||[],D.value=e.data?.total??w.value.length,J.value=!!e.data?.truncated}catch(s){console.error("kvList failed",s),V.notify({title:"Failed to load KV",message:s?.response?.data?.error?.message||"Unknown error",danger:!0})}finally{C.value=!1}};let N=null;const q=()=>{clearTimeout(N),N=setTimeout(v,250)},Y=s=>{o.row=s,o.text=X(s.value),o.ttlSeconds=s.expires_at?Math.max(0,Math.floor((new Date(s.expires_at)-Date.now())/1e3)):0,o.error="",o.open=!0},A=async()=>{let s;try{s=JSON.parse(o.text)}catch(e){o.error="Invalid JSON: "+e.message;return}o.error="",x.value=!0;try{await B(b.value,o.row.key,{value:s,ttl_seconds:o.ttlSeconds||0}),o.open=!1,await v()}catch(e){o.error=e?.response?.data?.error?.message||"Save failed"}finally{x.value=!1}},G=async()=>{!o.row||!await V.ask({title:"Delete key?",message:`"${o.row.key}" will be removed from this function's KV store. This cannot be undone.`,confirmLabel:"Delete",danger:!0})||(await O(o.row.key),o.open=!1)},H=()=>{n.key="",n.text="",n.ttlSeconds=0,n.error="",n.open=!0},Q=async()=>{const s=n.key.trim();if(!s){n.error="Key is required";return}let e=n.text.trim()||'""',a;try{a=JSON.parse(e)}catch(r){n.error="Invalid JSON: "+r.message;return}n.error="",x.value=!0;try{await B(b.value,s,{value:a,ttl_seconds:n.ttlSeconds||0}),n.open=!1,await v()}catch(r){n.error=r?.response?.data?.error?.message||"Save failed"}finally{x.value=!1}},W=async s=>{await V.ask({title:"Delete key?",message:`"${s.key}" will be removed from this function's KV store.`,confirmLabel:"Delete",danger:!0})&&await O(s.key)},O=async s=>{try{await ce(b.value,s),await v()}catch(e){V.notify({title:"Failed to delete key",message:e?.response?.data?.error?.message||"Unknown error",danger:!0})}},M=(s,e=3)=>{if(e<=0||typeof s!="string")return s;const a=s.trim();if(!a||!'{["tfn0123456789-'.includes(a[0]))return s;try{const r=JSON.parse(a);return M(r,e-1)}catch{return s}},X=s=>{const e=M(s);try{return JSON.stringify(e,null,2)}catch{return String(s)}},Z=s=>{if(s==null)return T;const e=M(s);if(typeof e=="string"){const a=JSON.stringify(e);return a.length>80?a.slice(0,80)+"…":a}try{const a=JSON.stringify(e);return a.length>80?a.slice(0,80)+"…":a}catch{return String(e)}},z=s=>s==null?T:s<1024?s+" B":s<1024*1024?(s/1024).toFixed(1)+" KB":(s/1024/1024).toFixed(1)+" MB",ee=s=>{if(!s)return T;const e=Date.now()-new Date(s).getTime();if(e<0)return"just now";const a=Math.floor(e/1e3);if(a<60)return a+"s ago";const r=Math.floor(a/60);if(r<60)return r+"m ago";const c=Math.floor(r/60);return c<24?c+"h ago":Math.floor(c/24)+"d ago"},te=s=>s?new Date(s).toLocaleString():T,$=s=>{const e=new Date(s).getTime()-Date.now();if(e<=0)return"expired";const a=Math.floor(e/1e3);if(a<60)return"in "+a+"s";const r=Math.floor(a/60);if(r<60)return"in "+r+"m";const c=Math.floor(r/60);return c<24?"in "+c+"h "+r%60+"m":"in "+Math.floor(c/24)+"d "+c%24+"h"},j=s=>{if(!s)return"text-foreground-muted";const e=new Date(s).getTime()-Date.now();return e<=6e4?"text-red-400":e<=5*6e4?"text-amber-400":"text-foreground-muted"};return re(v),oe(v),ne(()=>{clearTimeout(N)}),(s,e)=>{const a=de("router-link");return f(),u("div",ve,[t("div",ge,[t("div",null,[e[15]||(e[15]=t("h1",{class:"text-xl font-semibold text-white tracking-tight"}," KV Store ",-1)),t("p",ye,[e[13]||(e[13]=i(" Per-function key/value state for ",-1)),d(a,{to:`/functions/${b.value}`,class:"text-white underline"},{default:p(()=>[i(l(b.value),1)]),_:1},8,["to"]),e[14]||(e[14]=i(" Values are JSON, optional TTL. ",-1))])]),t("div",be,[t("span",we,[i(l(D.value)+" "+l(D.value===1?"key":"keys")+" ",1),L.value>0?(f(),u("span",ke,"· "+l(z(L.value)),1)):g("",!0)]),d(y,{variant:"secondary",size:"sm",onClick:v},{default:p(()=>[d(k(me),{class:U(["w-3.5 h-3.5",{"animate-spin":C.value}])},null,8,["class"]),e[16]||(e[16]=i(" Refresh ",-1))]),_:1}),d(y,{size:"sm",onClick:e[0]||(e[0]=r=>H())},{default:p(()=>[d(k(fe),{class:"w-3.5 h-3.5"}),e[17]||(e[17]=i(" Set key ",-1))]),_:1})])]),t("div",he,[t("div",_e,[d(k(ae),{class:"w-3.5 h-3.5 absolute left-2.5 top-1/2 -translate-y-1/2 text-foreground-muted/60 pointer-events-none"}),h(t("input",{"onUpdate:modelValue":e[1]||(e[1]=r=>m.value=r),placeholder:"Search by key prefix… (e.g. user:)",class:"w-full bg-background border border-border rounded-md pl-8 pr-3 py-1.5 text-xs text-foreground placeholder-foreground-muted/60 focus:outline-none focus:border-white",onInput:q},null,544),[[_,m.value]])]),m.value?(f(),u("button",{key:0,class:"text-[11px] text-foreground-muted hover:text-white px-2 py-1.5 transition-colors",onClick:e[2]||(e[2]=r=>{m.value="",v()})}," Clear ")):g("",!0),J.value?(f(),u("span",Se," Showing first "+l(w.value.length)+". Narrow the prefix to see more. ",1)):g("",!0)]),t("div",Te,[t("table",Ve,[e[23]||(e[23]=t("thead",{class:"text-xs text-foreground-muted uppercase bg-surface border-b border-border"},[t("tr",null,[t("th",{class:"px-4 py-3"},"Key"),t("th",{class:"px-4 py-3 hidden md:table-cell"},"Value preview"),t("th",{class:"px-4 py-3 w-28 hidden sm:table-cell"},"TTL"),t("th",{class:"px-4 py-3 w-20 hidden lg:table-cell"},"Size"),t("th",{class:"px-4 py-3 w-28 hidden md:table-cell"},"Updated"),t("th",{class:"px-4 py-3 w-10 text-right"})])],-1)),t("tbody",Ce,[(f(!0),u(K,null,le(w.value,r=>(f(),u("tr",{key:r.key,class:"hover:bg-surface/40 cursor-pointer transition-colors",onClick:c=>Y(r)},[t("td",Ne,l(r.key),1),t("td",Me,l(Z(r.value)),1),t("td",ze,[r.expires_at?(f(),u("span",{key:0,class:U(["text-xs",j(r.expires_at)])},l($(r.expires_at)),3)):(f(),u("span",Ke,l(k(T)),1))]),t("td",Ue,l(z(r.size_bytes)),1),t("td",Je,l(ee(r.updated_at)),1),t("td",{class:"px-4 py-3 text-right",onClick:e[3]||(e[3]=pe(()=>{},["stop"]))},[d(xe,{icon:k(R),variant:"danger",title:"Delete key",onClick:c=>W(r)},null,8,["icon","onClick"])])],8,De))),128)),!C.value&&!w.value.length?(f(),u("tr",Le,[t("td",Oe,[m.value?(f(),u(K,{key:0},[e[18]||(e[18]=i(" No keys match ",-1)),t("code",$e,l(m.value),1),e[19]||(e[19]=i(". ",-1))],64)):(f(),u(K,{key:1},[e[20]||(e[20]=i(" No keys yet. Your function will write here when it calls ",-1)),e[21]||(e[21]=t("code",{class:"bg-surface px-1.5 py-0.5 rounded text-xs font-mono"},"orva.kv.put(...)",-1)),e[22]||(e[22]=i(". ",-1))],64))])])):g("",!0)])])]),d(I,{modelValue:o.open,"onUpdate:modelValue":e[7]||(e[7]=r=>o.open=r),title:o.row?o.row.key:"Inspect key",width:"640px"},{footer:p(()=>[t("div",He,[d(y,{variant:"danger",size:"sm",disabled:x.value,onClick:G},{default:p(()=>[d(k(R),{class:"w-3.5 h-3.5"}),e[30]||(e[30]=i(" Delete ",-1))]),_:1},8,["disabled"]),t("div",Qe,[d(y,{variant:"ghost",size:"sm",onClick:e[6]||(e[6]=r=>o.open=!1)},{default:p(()=>[...e[31]||(e[31]=[i(" Cancel ",-1)])]),_:1}),d(y,{size:"sm",disabled:x.value,loading:x.value,onClick:A},{default:p(()=>[...e[32]||(e[32]=[i(" Save ",-1)])]),_:1},8,["disabled","loading"])])])]),default:p(()=>[o.row?(f(),u("div",je,[t("div",Fe,[t("div",Pe,[e[24]||(e[24]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Key",-1)),t("div",Be,l(o.row.key),1)]),t("div",Ie,[e[25]||(e[25]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"TTL",-1)),t("div",{class:U(["text-xs text-white font-mono",j(o.row.expires_at)])},l(o.row.expires_at?$(o.row.expires_at):"Never"),3)]),t("div",Re,[e[26]||(e[26]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Updated",-1)),t("div",Ee,l(te(o.row.updated_at)),1)]),t("div",qe,[e[27]||(e[27]=t("div",{class:"text-[10px] uppercase tracking-wider text-foreground-muted mb-1"},"Size",-1)),t("div",Ye,l(z(o.row.size_bytes)),1)])]),t("div",null,[e[28]||(e[28]=t("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"}," TTL seconds (0 = never) ",-1)),h(t("input",{"onUpdate:modelValue":e[4]||(e[4]=r=>o.ttlSeconds=r),type:"number",min:"0",class:"mt-2 w-full bg-surface border border-border rounded px-3 py-2 text-sm text-white font-mono focus:outline-none focus:border-white"},null,512),[[_,o.ttlSeconds,void 0,{number:!0}]])]),t("div",null,[t("div",Ae,[e[29]||(e[29]=t("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"},"Value (JSON)",-1)),o.error?(f(),u("span",Ge,l(o.error),1)):g("",!0)]),h(t("textarea",{"onUpdate:modelValue":e[5]||(e[5]=r=>o.text=r),rows:"14",spellcheck:"false",class:"w-full bg-surface border border-border rounded p-3 text-xs text-white font-mono leading-relaxed focus:outline-none focus:border-white whitespace-pre overflow-x-auto"},null,512),[[_,o.text]])])])):g("",!0)]),_:1},8,["modelValue","title"]),d(I,{modelValue:n.open,"onUpdate:modelValue":e[12]||(e[12]=r=>n.open=r),title:"Set key",width:"640px"},{footer:p(()=>[t("div",et,[d(y,{variant:"ghost",size:"sm",onClick:e[11]||(e[11]=r=>n.open=!1)},{default:p(()=>[...e[36]||(e[36]=[i(" Cancel ",-1)])]),_:1}),d(y,{size:"sm",disabled:x.value||!n.key.trim(),loading:x.value,onClick:Q},{default:p(()=>[...e[37]||(e[37]=[i(" Save ",-1)])]),_:1},8,["disabled","loading"])])]),default:p(()=>[t("div",We,[t("div",null,[e[33]||(e[33]=t("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"},"Key",-1)),h(t("input",{"onUpdate:modelValue":e[8]||(e[8]=r=>n.key=r),placeholder:"e.g. user:42",class:"mt-2 w-full bg-surface border border-border rounded px-3 py-2 text-sm text-white font-mono focus:outline-none focus:border-white",spellcheck:"false"},null,512),[[_,n.key]])]),t("div",null,[e[34]||(e[34]=t("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"}," TTL seconds (0 = never) ",-1)),h(t("input",{"onUpdate:modelValue":e[9]||(e[9]=r=>n.ttlSeconds=r),type:"number",min:"0",class:"mt-2 w-full bg-surface border border-border rounded px-3 py-2 text-sm text-white font-mono focus:outline-none focus:border-white"},null,512),[[_,n.ttlSeconds,void 0,{number:!0}]])]),t("div",null,[t("div",Xe,[e[35]||(e[35]=t("label",{class:"text-xs uppercase tracking-wider text-foreground-muted"},"Value (JSON)",-1)),n.error?(f(),u("span",Ze,l(n.error),1)):g("",!0)]),h(t("textarea",{"onUpdate:modelValue":e[10]||(e[10]=r=>n.text=r),rows:"14",spellcheck:"false",placeholder:'{"hello": "world"}',class:"w-full bg-surface border border-border rounded p-3 text-xs text-white font-mono leading-relaxed focus:outline-none focus:border-white whitespace-pre overflow-x-auto"},null,512),[[_,n.text]])])])]),_:1},8,["modelValue"])])}}};export{it as default}; diff --git a/backend/internal/server/ui_dist/assets/Login-CCs893z5.js b/backend/internal/server/ui_dist/assets/Login-CCs893z5.js new file mode 100644 index 0000000..0d1c331 --- /dev/null +++ b/backend/internal/server/ui_dist/assets/Login-CCs893z5.js @@ -0,0 +1 @@ +import{c as x,u as v,o as h,a as c,b as s,d as n,w,e as m,v as p,f,t as y,g as k,h as _,_ as S,i as I,r as u,j as g,O as N,k as U}from"./index-CmY58qNN.js";import{C as V}from"./circle-alert-zkHrlU5I.js";const E=x("log-in",[["path",{d:"m10 17 5-5-5-5",key:"1bsop3"}],["path",{d:"M15 12H3",key:"6jk70r"}],["path",{d:"M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4",key:"u53s6r"}]]),L={class:"min-h-screen flex items-center justify-center bg-background p-4"},M={class:"w-full max-w-md"},O={class:"text-center mb-8"},j={class:"flex items-center justify-center gap-3 mb-4"},A={class:"bg-surface border border-border rounded-lg shadow-lg p-8"},B=["disabled"],C=["disabled"],D={key:0,class:"bg-danger-tint border border-danger-ring rounded-md px-4 py-3 flex items-start gap-2"},R={class:"text-sm text-danger-fg"},G={__name:"Login",setup(q){const d=I(),i=v();h(async()=>{await i.fetchAuthStatus()===!1&&d.replace("/onboarding")});const t=u({username:"",password:""}),a=u(""),r=u(!1),b=async()=>{a.value="",r.value=!0;const o=await i.login(t.value.username,t.value.password);r.value=!1,o.success?d.push("/"):o.code==="ONBOARDING_REQUIRED"?d.push("/onboarding"):a.value=o.error};return(o,e)=>(g(),c("div",L,[s("div",M,[s("div",O,[s("div",j,[n(N,{class:"w-12 h-12"}),e[2]||(e[2]=s("h1",{class:"text-3xl font-bold tracking-tight text-foreground"}," Orva ",-1))]),e[3]||(e[3]=s("p",{class:"text-foreground-muted text-sm"}," Serverless Platform ",-1))]),s("div",A,[e[7]||(e[7]=s("h2",{class:"text-xl font-semibold text-foreground mb-6"}," Sign In ",-1)),s("form",{class:"space-y-5",onSubmit:w(b,["prevent"])},[s("div",null,[e[4]||(e[4]=s("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-2"}," Username ",-1)),m(s("input",{"onUpdate:modelValue":e[0]||(e[0]=l=>t.value.username=l),type:"text",required:"",class:"w-full bg-background border border-border rounded-md px-4 py-2.5 text-sm text-foreground placeholder-foreground-muted focus:outline-none focus:ring-2 focus:ring-primary transition-colors",placeholder:"Enter your username",disabled:r.value},null,8,B),[[p,t.value.username]])]),s("div",null,[e[5]||(e[5]=s("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-2"}," Password ",-1)),m(s("input",{"onUpdate:modelValue":e[1]||(e[1]=l=>t.value.password=l),type:"password",required:"",class:"w-full bg-background border border-border rounded-md px-4 py-2.5 text-sm text-foreground placeholder-foreground-muted focus:outline-none focus:ring-2 focus:ring-primary transition-colors",placeholder:"Enter your password",disabled:r.value},null,8,C),[[p,t.value.password]])]),a.value?(g(),c("div",D,[n(f(V),{class:"w-5 h-5 text-danger-fg shrink-0 mt-0.5"}),s("p",R,y(a.value),1)])):k("",!0),n(S,{type:"submit",class:"w-full",loading:r.value,disabled:!t.value.username||!t.value.password||r.value},{default:_(()=>[n(f(E),{class:"w-4 h-4"}),e[6]||(e[6]=U(" Sign In ",-1))]),_:1},8,["loading","disabled"])],32)])])]))}};export{G as default}; diff --git a/backend/internal/server/ui_dist/assets/Login-PkbjKPoy.js b/backend/internal/server/ui_dist/assets/Login-PkbjKPoy.js deleted file mode 100644 index 2469e0e..0000000 --- a/backend/internal/server/ui_dist/assets/Login-PkbjKPoy.js +++ /dev/null @@ -1 +0,0 @@ -import{c as x,u as v,o as h,a as c,b as s,d,w,e as m,v as p,f,t as y,g as k,h as _,_ as S,i as I,r as u,j as b,O as N,k as U}from"./index-WXhXpu06.js";import{C as V}from"./circle-alert-CvukaQh2.js";const E=x("log-in",[["path",{d:"m10 17 5-5-5-5",key:"1bsop3"}],["path",{d:"M15 12H3",key:"6jk70r"}],["path",{d:"M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4",key:"u53s6r"}]]),L={class:"min-h-screen flex items-center justify-center bg-background p-4"},M={class:"w-full max-w-md"},O={class:"text-center mb-8"},j={class:"flex items-center justify-center gap-3 mb-4"},A={class:"bg-surface border border-border rounded-lg shadow-2xl shadow-black/50 p-8"},B=["disabled"],C=["disabled"],D={key:0,class:"bg-error/10 border border-error/30 rounded-md px-4 py-3 flex items-start gap-2"},R={class:"text-sm text-error"},G={__name:"Login",setup(q){const l=I(),i=v();h(async()=>{await i.fetchAuthStatus()===!1&&l.replace("/onboarding")});const r=u({username:"",password:""}),a=u(""),o=u(!1),g=async()=>{a.value="",o.value=!0;const t=await i.login(r.value.username,r.value.password);o.value=!1,t.success?l.push("/"):t.code==="ONBOARDING_REQUIRED"?l.push("/onboarding"):a.value=t.error};return(t,e)=>(b(),c("div",L,[s("div",M,[s("div",O,[s("div",j,[d(N,{class:"w-12 h-12"}),e[2]||(e[2]=s("h1",{class:"text-3xl font-bold tracking-tight text-foreground"}," Orva ",-1))]),e[3]||(e[3]=s("p",{class:"text-foreground-muted text-sm"}," Serverless Platform ",-1))]),s("div",A,[e[7]||(e[7]=s("h2",{class:"text-xl font-semibold text-foreground mb-6"}," Sign In ",-1)),s("form",{class:"space-y-5",onSubmit:w(g,["prevent"])},[s("div",null,[e[4]||(e[4]=s("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-2"}," Username ",-1)),m(s("input",{"onUpdate:modelValue":e[0]||(e[0]=n=>r.value.username=n),type:"text",required:"",class:"w-full bg-background border border-border rounded-md px-4 py-2.5 text-sm text-foreground placeholder-foreground-muted focus:outline-none focus:ring-2 focus:ring-primary transition-colors",placeholder:"Enter your username",disabled:o.value},null,8,B),[[p,r.value.username]])]),s("div",null,[e[5]||(e[5]=s("label",{class:"text-xs font-medium text-foreground-muted uppercase tracking-wide block mb-2"}," Password ",-1)),m(s("input",{"onUpdate:modelValue":e[1]||(e[1]=n=>r.value.password=n),type:"password",required:"",class:"w-full bg-background border border-border rounded-md px-4 py-2.5 text-sm text-foreground placeholder-foreground-muted focus:outline-none focus:ring-2 focus:ring-primary transition-colors",placeholder:"Enter your password",disabled:o.value},null,8,C),[[p,r.value.password]])]),a.value?(b(),c("div",D,[d(f(V),{class:"w-5 h-5 text-error shrink-0 mt-0.5"}),s("p",R,y(a.value),1)])):k("",!0),d(S,{type:"submit",class:"w-full",loading:o.value,disabled:!r.value.username||!r.value.password||o.value},{default:_(()=>[d(f(E),{class:"w-4 h-4"}),e[6]||(e[6]=U(" Sign In ",-1))]),_:1},8,["loading","disabled"])],32)])])]))}};export{G as default}; diff --git a/backend/internal/server/ui_dist/assets/Modal--oERTKOu.js b/backend/internal/server/ui_dist/assets/Modal--oERTKOu.js deleted file mode 100644 index b287b2f..0000000 --- a/backend/internal/server/ui_dist/assets/Modal--oERTKOu.js +++ /dev/null @@ -1 +0,0 @@ -import{c as k,j as t,a,k as w,t as d,g as s,b as o,s as y,n as u,a2 as g,a_ as v,a$ as V,o as S,y as $,d as b,h as C,w as z,f as B,a3 as j,aH as x,T as I,aI as M,r as N,q as T}from"./index-WXhXpu06.js";const Q=k("shield-check",[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z",key:"oel41y"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]]),q={class:"flex flex-col gap-1.5 w-full"},E={key:0,class:"text-xs font-medium text-foreground-muted uppercase tracking-wide"},L={key:0,class:"text-red-500"},R={class:"relative"},D=["type","value","placeholder","disabled"],F={key:0,class:"absolute left-3 top-1/2 -translate-y-1/2 text-foreground-muted"},O={key:1,class:"text-xs text-red-500"},H={key:2,class:"text-xs text-foreground-muted"},W={__name:"Input",props:{modelValue:{type:[String,Number],default:""},label:{type:String,default:""},type:{type:String,default:"text"},placeholder:{type:String,default:""},error:{type:String,default:""},hint:{type:String,default:""},icon:{type:Object,default:null},required:Boolean,disabled:Boolean},emits:["update:modelValue"],setup(e){return(n,l)=>(t(),a("div",q,[e.label?(t(),a("label",E,[w(d(e.label)+" ",1),e.required?(t(),a("span",L,"*")):s("",!0)])):s("",!0),o("div",R,[o("input",{type:e.type,value:e.modelValue,class:y(["w-full bg-background border border-border rounded-md px-3 py-2 text-base sm:text-sm text-foreground placeholder-foreground-muted/50 focus:outline-none focus:ring-1 focus:ring-white focus:border-white transition-colors duration-200",{"pl-9":e.icon}]),placeholder:e.placeholder,disabled:e.disabled,onInput:l[0]||(l[0]=i=>n.$emit("update:modelValue",i.target.value))},null,42,D),e.icon?(t(),a("div",F,[(t(),u(g(e.icon),{class:"w-4 h-4"}))])):s("",!0)]),e.error?(t(),a("span",O,d(e.error),1)):s("",!0),e.hint&&!e.error?(t(),a("span",H,d(e.hint),1)):s("",!0)]))}},K={class:"flex items-center justify-between px-5 py-3 border-b border-border shrink-0"},U={class:"flex items-center gap-2 min-w-0"},X=["aria-label"],A={class:"p-5 overflow-y-auto scrollable flex-1 max-h-[calc(100dvh-9rem)] sm:max-h-[70vh]"},G={key:0,class:"px-5 py-3 border-t border-border flex items-center justify-end gap-2 bg-surface/40 shrink-0"},Y={__name:"Modal",props:{modelValue:{type:Boolean,default:!1},title:{type:String,required:!0},icon:{type:[Object,Function],default:null},size:{type:String,default:"md",validator:e=>["sm","md","lg","xl"].includes(e)}},emits:["update:modelValue"],setup(e,{emit:n}){const l=e,i=n,m=`modal-title-${Math.random().toString(36).slice(2,10)}`,f=N(null);v(f,V(l,"modelValue"));const p=T(()=>{switch(l.size){case"sm":return"sm:max-w-sm";case"lg":return"sm:max-w-2xl";case"xl":return"sm:max-w-4xl";default:return"sm:max-w-lg"}}),c=()=>i("update:modelValue",!1),h=r=>{r.key==="Escape"&&l.modelValue&&c()};return S(()=>window.addEventListener("keydown",h)),$(()=>window.removeEventListener("keydown",h)),(r,J)=>(t(),u(M,{to:"body"},[b(I,{name:"fade"},{default:C(()=>[e.modelValue?(t(),a("div",{key:0,class:"fixed inset-0 z-40 flex items-stretch sm:items-center justify-center overflow-y-auto bg-black/60 backdrop-blur-sm pt-safe pb-safe pl-safe pr-safe p-2 sm:p-4",onClick:z(c,["self"])},[o("div",{ref_key:"dialogRoot",ref:f,class:y(["w-full bg-background border border-border rounded-lg shadow-xl my-0 sm:my-auto flex flex-col max-w-full",p.value]),role:"dialog","aria-modal":"true","aria-labelledby":m},[o("header",K,[o("div",U,[e.icon?(t(),u(g(e.icon),{key:0,class:"w-4 h-4 text-foreground-muted shrink-0"})):s("",!0),o("h3",{id:m,class:"text-sm font-semibold text-white tracking-tight truncate"},d(e.title),1)]),o("button",{class:"p-1.5 -mr-1.5 rounded text-foreground-muted hover:text-white hover:bg-surface-hover transition-colors touch-expand-iconbtn shrink-0","aria-label":`Close ${e.title}`,onClick:c},[b(B(j),{class:"w-4 h-4"})],8,X)]),o("div",A,[x(r.$slots,"default")]),r.$slots.footer?(t(),a("footer",G,[x(r.$slots,"footer")])):s("",!0)],2)])):s("",!0)]),_:3})]))}};export{Q as S,Y as _,W as a}; diff --git a/backend/internal/server/ui_dist/assets/Modal-zN5wBHcp.js b/backend/internal/server/ui_dist/assets/Modal-zN5wBHcp.js new file mode 100644 index 0000000..2ee3ef9 --- /dev/null +++ b/backend/internal/server/ui_dist/assets/Modal-zN5wBHcp.js @@ -0,0 +1 @@ +import{b1 as p,b2 as y,o as w,y as g,j as o,n as c,d as m,h as k,a as u,w as v,b as t,s as V,K as C,g as r,t as _,f as j,ac as z,Q as f,N as B,U as S,r as $,q as E}from"./index-CmY58qNN.js";const M={class:"flex items-center justify-between px-5 py-3 border-b border-border shrink-0"},N={class:"flex items-center gap-2 min-w-0"},R=["aria-label"],T={class:"p-5 overflow-y-auto scrollable flex-1 max-h-[calc(100dvh-9rem)] sm:max-h-[70vh]"},q={key:0,class:"px-5 py-3 border-t border-border flex items-center justify-end gap-2 bg-surface/40 shrink-0"},K={__name:"Modal",props:{modelValue:{type:Boolean,default:!1},title:{type:String,required:!0},icon:{type:[Object,Function],default:null},size:{type:String,default:"md",validator:e=>["sm","md","lg","xl"].includes(e)}},emits:["update:modelValue"],setup(e,{emit:b}){const a=e,h=b,n=`modal-title-${Math.random().toString(36).slice(2,10)}`,i=$(null);p(i,y(a,"modelValue"));const x=E(()=>{switch(a.size){case"sm":return"sm:max-w-sm";case"lg":return"sm:max-w-2xl";case"xl":return"sm:max-w-4xl";default:return"sm:max-w-lg"}}),l=()=>h("update:modelValue",!1),d=s=>{s.key==="Escape"&&a.modelValue&&l()};return w(()=>window.addEventListener("keydown",d)),g(()=>window.removeEventListener("keydown",d)),(s,D)=>(o(),c(S,{to:"body"},[m(B,{name:"fade"},{default:k(()=>[e.modelValue?(o(),u("div",{key:0,class:"fixed inset-0 z-40 flex items-stretch sm:items-center justify-center overflow-y-auto bg-black/60 backdrop-blur-sm pt-safe pb-safe pl-safe pr-safe p-2 sm:p-4",onClick:v(l,["self"])},[t("div",{ref_key:"dialogRoot",ref:i,class:V(["w-full bg-background border border-border rounded-lg shadow-xl my-0 sm:my-auto flex flex-col max-w-full",x.value]),role:"dialog","aria-modal":"true","aria-labelledby":n},[t("header",M,[t("div",N,[e.icon?(o(),c(C(e.icon),{key:0,class:"w-4 h-4 text-foreground-muted shrink-0"})):r("",!0),t("h3",{id:n,class:"text-sm font-semibold text-white tracking-tight truncate"},_(e.title),1)]),t("button",{class:"p-1.5 -mr-1.5 rounded text-foreground-muted hover:text-white hover:bg-surface-hover transition-colors touch-expand-iconbtn shrink-0","aria-label":`Close ${e.title}`,onClick:l},[m(j(z),{class:"w-4 h-4"})],8,R)]),t("div",T,[f(s.$slots,"default")]),s.$slots.footer?(o(),u("footer",q,[f(s.$slots,"footer")])):r("",!0)],2)])):r("",!0)]),_:3})]))}};export{K as _}; diff --git a/backend/internal/server/ui_dist/assets/NotFound-BYJtKtzW.js b/backend/internal/server/ui_dist/assets/NotFound-BijYJdCk.js similarity index 89% rename from backend/internal/server/ui_dist/assets/NotFound-BYJtKtzW.js rename to backend/internal/server/ui_dist/assets/NotFound-BijYJdCk.js index fa79f64..92ffd2e 100644 --- a/backend/internal/server/ui_dist/assets/NotFound-BYJtKtzW.js +++ b/backend/internal/server/ui_dist/assets/NotFound-BijYJdCk.js @@ -1 +1 @@ -import{a as u,b as o,d as e,t as m,f as r,k as n,q as x,i as c,a1 as p,j as f,O as g}from"./index-WXhXpu06.js";import{A as b}from"./arrow-left-QkYKhGek.js";import{B as v}from"./book-open-Jrxx9Bqu.js";const h={class:"min-h-screen flex items-center justify-center bg-background px-6 py-16"},k={class:"max-w-lg w-full"},w={class:"flex items-center gap-3 mb-10"},y={class:"bg-surface border border-border rounded-md px-3 py-2 mb-8 overflow-x-auto"},O={class:"text-xs font-mono text-foreground break-all"},_={class:"flex flex-wrap gap-3"},j={__name:"NotFound",setup(B){const a=p(),s=c(),d=x(()=>a.fullPath||"/"),i=()=>s.push("/functions"),l=()=>s.push("/docs");return(N,t)=>(f(),u("div",h,[o("div",k,[o("div",w,[e(g,{class:"w-7 h-7"}),t[0]||(t[0]=o("span",{class:"font-semibold tracking-tight text-foreground text-lg"},"Orva",-1))]),t[3]||(t[3]=o("p",{class:"text-xs uppercase tracking-[0.2em] text-foreground-muted mb-3"}," Error 404 ",-1)),t[4]||(t[4]=o("h1",{class:"text-3xl sm:text-4xl font-semibold text-foreground mb-4 leading-tight"}," Page not found. ",-1)),t[5]||(t[5]=o("p",{class:"text-sm text-foreground-muted mb-2"}," Nothing is mapped to this URL on this Orva instance. ",-1)),t[6]||(t[6]=o("p",{class:"text-sm text-foreground-muted mb-8"}," You tried: ",-1)),o("div",y,[o("code",O,m(d.value),1)]),o("div",_,[o("button",{class:"inline-flex items-center gap-2 px-4 py-2 rounded-md bg-white text-black text-sm font-medium hover:bg-white/90 transition-colors",onClick:i},[e(r(b),{class:"w-4 h-4"}),t[1]||(t[1]=n(" Go to Functions ",-1))]),o("button",{class:"inline-flex items-center gap-2 px-4 py-2 rounded-md border border-border text-sm text-foreground hover:bg-surface-hover transition-colors",onClick:l},[e(r(v),{class:"w-4 h-4"}),t[2]||(t[2]=n(" Open docs ",-1))])])])]))}};export{j as default}; +import{a as u,b as o,d as e,t as m,f as r,k as n,q as x,i as c,ab as p,j as f,O as g}from"./index-CmY58qNN.js";import{A as b}from"./arrow-left-D0tEKPgG.js";import{B as v}from"./book-open-DME4yN8Y.js";const h={class:"min-h-screen flex items-center justify-center bg-background px-6 py-16"},k={class:"max-w-lg w-full"},w={class:"flex items-center gap-3 mb-10"},y={class:"bg-surface border border-border rounded-md px-3 py-2 mb-8 overflow-x-auto"},O={class:"text-xs font-mono text-foreground break-all"},_={class:"flex flex-wrap gap-3"},j={__name:"NotFound",setup(B){const a=p(),s=c(),d=x(()=>a.fullPath||"/"),i=()=>s.push("/functions"),l=()=>s.push("/docs");return(N,t)=>(f(),u("div",h,[o("div",k,[o("div",w,[e(g,{class:"w-7 h-7"}),t[0]||(t[0]=o("span",{class:"font-semibold tracking-tight text-foreground text-lg"},"Orva",-1))]),t[3]||(t[3]=o("p",{class:"text-xs uppercase tracking-[0.2em] text-foreground-muted mb-3"}," Error 404 ",-1)),t[4]||(t[4]=o("h1",{class:"text-3xl sm:text-4xl font-semibold text-foreground mb-4 leading-tight"}," Page not found. ",-1)),t[5]||(t[5]=o("p",{class:"text-sm text-foreground-muted mb-2"}," Nothing is mapped to this URL on this Orva instance. ",-1)),t[6]||(t[6]=o("p",{class:"text-sm text-foreground-muted mb-8"}," You tried: ",-1)),o("div",y,[o("code",O,m(d.value),1)]),o("div",_,[o("button",{class:"inline-flex items-center gap-2 px-4 py-2 rounded-md bg-white text-black text-sm font-medium hover:bg-white/90 transition-colors",onClick:i},[e(r(b),{class:"w-4 h-4"}),t[1]||(t[1]=n(" Go to Functions ",-1))]),o("button",{class:"inline-flex items-center gap-2 px-4 py-2 rounded-md border border-border text-sm text-foreground hover:bg-surface-hover transition-colors",onClick:l},[e(r(v),{class:"w-4 h-4"}),t[2]||(t[2]=n(" Open docs ",-1))])])])]))}};export{j as default}; diff --git a/backend/internal/server/ui_dist/assets/Onboarding-Cmx-esoY.js b/backend/internal/server/ui_dist/assets/Onboarding-DPm1ZBaj.js similarity index 98% rename from backend/internal/server/ui_dist/assets/Onboarding-Cmx-esoY.js rename to backend/internal/server/ui_dist/assets/Onboarding-DPm1ZBaj.js index 6c6dbd2..13ab6da 100644 --- a/backend/internal/server/ui_dist/assets/Onboarding-Cmx-esoY.js +++ b/backend/internal/server/ui_dist/assets/Onboarding-DPm1ZBaj.js @@ -1,4 +1,4 @@ -import{c as S,u as P,o as T,a as p,b as s,d as f,l as $,w as z,e as k,v as B,m as D,f as b,g as _,n as C,F as R,p as E,t as V,h as F,_ as H,i as I,r as g,q,j as l,O as Z,s as M,k as G}from"./index-WXhXpu06.js";import{C as J}from"./copy-HWkx-vox.js";import{C as K}from"./circle-alert-CvukaQh2.js";const Q=S("eye-off",[["path",{d:"M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49",key:"ct8e1f"}],["path",{d:"M14.084 14.158a3 3 0 0 1-4.242-4.242",key:"151rxh"}],["path",{d:"M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143",key:"13bj9a"}],["path",{d:"m2 2 20 20",key:"1ooewy"}]]);const W=S("eye",[["path",{d:"M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0",key:"1nclc0"}],["circle",{cx:"12",cy:"12",r:"3",key:"1v7zrd"}]]),X={class:"min-h-screen flex flex-col items-center justify-center bg-background pt-safe pb-safe pl-safe pr-safe px-page py-8 sm:py-12"},Y={class:"w-full max-w-md space-y-8"},ee={class:"space-y-3"},se={class:"flex items-center gap-3"},te=["disabled"],oe={class:"flex flex-col sm:flex-row gap-2"},ae={class:"relative flex-1 min-w-0"},re=["type","disabled"],ne={class:"absolute right-1.5 top-1/2 -translate-y-1/2 flex gap-0.5"},de=["disabled"],le=["disabled","aria-label","title"],ue=["disabled"],ie={class:"mt-3 grid grid-cols-2 gap-y-1.5 gap-x-3"},ce={key:0,class:"bg-danger/10 border border-danger/30 rounded-md px-4 py-3 flex items-start gap-3"},pe={class:"text-sm text-danger"},xe={__name:"Onboarding",setup(me){const x=I(),v=P(),o=g({username:"orva",password:""}),m=g(""),n=g(!1),u=g(!1),w=q(()=>({length:o.value.password.length>=10,lower:/[a-z]/.test(o.value.password),upper:/[A-Z]/.test(o.value.password),digit:/[0-9]/.test(o.value.password),symbol:/[^A-Za-z0-9]/.test(o.value.password)})),U=q(()=>{const t=w.value;return t.length&&t.lower&&t.upper&&t.digit&&t.symbol}),A=t=>({length:"10+ characters",lower:"Lowercase",upper:"Uppercase",digit:"Number",symbol:"Symbol"})[t]||t,O=()=>{const e="abcdefghijklmnopqrstuvwxyz",r="ABCDEFGHIJKLMNOPQRSTUVWXYZ",i="0123456789",h="!@#$%^&*()-_=+[]{}|;:,.<>?",L=e+r+i+h,c=a=>a[crypto.getRandomValues(new Uint32Array(1))[0]%a.length];let d=[c(e),c(r),c(i),c(h)];for(let a=d.length;a<16;a+=1)d.push(c(L));for(let a=d.length-1;a>0;a-=1){const y=crypto.getRandomValues(new Uint32Array(1))[0]%(a+1);[d[a],d[y]]=[d[y],d[a]]}o.value.password=d.join("")},N=async()=>{try{await navigator.clipboard.writeText(o.value.password)}catch(t){console.error("Failed to copy password:",t)}},j=async()=>{m.value="",n.value=!0;const t=await v.onboard(o.value.username,o.value.password);n.value=!1,t.success?x.push("/"):m.value=t.error};return T(async()=>{await v.fetchAuthStatus()&&x.push("/login")}),(t,e)=>(l(),p("div",X,[s("div",Y,[s("div",ee,[s("div",se,[f(Z,{class:"w-9 h-9"}),e[3]||(e[3]=s("span",{class:"text-2xl font-semibold tracking-tight text-white"},"Orva",-1))]),e[4]||(e[4]=s("h1",{class:"text-xl font-semibold text-white tracking-tight"}," Set up admin access ",-1)),e[5]||(e[5]=s("p",{class:"text-sm text-foreground-muted leading-relaxed"}," Create the root user for this Orva instance. This is a one-time setup and cannot be repeated; export your password somewhere durable before you continue. ",-1))]),e[10]||(e[10]=$(`