feat(dashboard): four-tab detail UI with routing + sandbox config

Restructures the script detail page from one wall of cards into a
focused four-tab layout. Each tab does one thing well:

  * Edit       source editor + Test invoke panel (the existing
               quick-iteration loop, unchanged in shape)
  * Routing    routes table + add-route form with live kind
               auto-detection + match-preview tool
  * Settings   name / description / timeout, plus a collapsible
               "Advanced sandbox" disclosure with the six Rhai
               limits (admin-ceiling-validated server-side)
  * Executions the existing log viewer, gets the room it deserves

Why four tabs (not two columns, not one tab per surface, not URL
sub-routes):
  * Two columns crowded out the routing UX, which is genuinely
    list+form-shaped and wants vertical space.
  * URL sub-routes (/scripts/{id}/routing) would let users
    bookmark a tab but cost a bunch of router files for a
    feature nobody asked for; tab state in $state is one line
    and gets us 95% of the UX.
  * Sandbox config tucked inside Settings' "Advanced" disclosure
    matches the 95%-never-touch reality without hiding it.

Routing tab highlights:
  * Path input + path-kind selector with live auto-detect: type
    /greet/:name and the kind selector flips to "param"; the
    user can override and the backend trusts the override (lets
    /greet/:name be matched literally if exact is selected).
  * Soft warning when selection mismatches the input shape — not
    blocking, with the rationale in the message.
  * Host kind auto-detect mirrors the path side (*.example.com →
    wildcard; non-empty literal → strict; empty → any).
  * Method dropdown defaults to ANY.
  * 409 conflicts render inline with the conflicting route's
    method/host/path so the user can read both at a glance.
  * Match-preview panel: synthetic URL + method → "matches THIS
    script" or "matches a DIFFERENT script" or "no match", with
    extracted params shown. Powered by /api/v1/admin/routes:match.
  * Full URLs in the routes list use the operator's
    PICLOUD_PUBLIC_BASE_URL from /version.

Settings tab:
  * Name / description / timeout editable from the UI for the
    first time (previously only source was).
  * "Advanced sandbox" details element collapsed by default;
    six number inputs with placeholder "(default)" meaning
    "use platform default for this knob". Save sends a fresh
    sandbox object; ceiling errors surface as the manager's 422
    message inline.

API client (dashboard/src/lib/api.ts):
  * Adds `Route`, `RouteInput`, `CheckRouteResponse`,
    `MatchRouteResponse`, `ScriptSandbox`, `VersionInfo` types.
  * Adds `api.routes.{listForScript,create,remove,check,match}`.
  * Adds `api.version()`.
  * `UpdateScriptInput` gains `sandbox`.

route-utils.ts:
  * `guessPathKind`, `guessHostKind` heuristics.
  * `pathKindMismatchWarning` for the soft-warning UX.

Verified live:
  /admin/                       → 200, dashboard HTML
  /admin/scripts/<id>           → SPA detail with all four tabs
  POST /api/v1/admin/scripts/<id>/routes → 201, route persists
  GET /demo/widget              → 200 (script answers via routing
                                    table after the dashboard's
                                    POST refreshes it)
  /version                      → public_base_url, schema 3, sdk 1.1

No backend changes — Commit 3 is purely additive UI on top of the
infrastructure Commits 1 and 2 already shipped.

Bumps: product 0.4.0 → 0.5.0 (user-visible UX change). Schema, SDK,
API, wire unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-23 22:07:36 +02:00
parent 07e2a62d98
commit ed462726de
7 changed files with 892 additions and 189 deletions

View File

@@ -0,0 +1,32 @@
import type { HostKind, PathKind } from './api';
/** Guess a path kind from the literal user input. The dashboard pre-fills
* the kind selector but the user can override (the backend trusts the
* selection — selecting `exact` for `/greet/:name` matches it literally). */
export function guessPathKind(raw: string): PathKind {
if (raw.endsWith('/*')) return 'prefix';
if (/(^|\/):[a-zA-Z_]/.test(raw)) return 'param';
return 'exact';
}
/** Guess a host kind. Empty → any, leading "*." → wildcard, else strict. */
export function guessHostKind(raw: string): HostKind {
if (!raw.trim()) return 'any';
if (raw.startsWith('*.')) return 'wildcard';
return 'strict';
}
/** Warn-not-block: does the user's selection match what we'd guess
* from the input? Used by the routing form for the soft mismatch
* hint. Returns null when no warning is needed. */
export function pathKindMismatchWarning(raw: string, kind: PathKind): string | null {
const guessed = guessPathKind(raw);
if (guessed === kind) return null;
const hint =
guessed === 'param'
? 'this looks like a param pattern (contains `:name`)'
: guessed === 'prefix'
? 'this looks like a prefix pattern (ends with `/*`)'
: 'this looks like a literal path';
return `Selected kind is "${kind}", but ${hint}. Routing will use the selected kind.`;
}