KLKLine FrontendDashboard · Preview · Embed shell

Admin widget builder

Widget save contract readiness

F029 maps the applied builder draft into backend preview contracts and keeps real persistence fail-closed until authenticated admin save/audit is available.

Checked at

۰۶:۳۳:۰۸

Static backend-source map from the uploaded backend controller paths.

Preview validate

READY

Create/update/validate preview endpoints exist, so the builder can prepare deterministic payloads before live persistence.

Direct save

BLOCKED

Direct save remains disabled until authenticated admin flow, ownership checks and audit evidence are wired.

Warnings

2

Warnings are expected for generic persisted CRUD endpoints.
F029 · preview-only save contract

Admin widget save readiness

This page intentionally does not collect credentials or perform persisted writes. It maps the applied Widget Builder draft into the backend preview payload shape used by `/api/admin/widget-instances/*-preview`, then keeps real create/update blocked until the authenticated admin save flow exists.

PASS

Widget instance registry contract

GET /api/admin/widget-instances/contract

auth: none

Backend exposes the widget instance registry handoff contract for frontend/admin save planning.

  • src/control-plane/widget-instance-registry/widget-instance-registry.controller.ts -> @Get("contract")
PASS

Widget instance schema metadata

GET /api/admin/widget-instances/schema

auth: none

Backend exposes schema metadata describing tenant/license/widget-type binding and validate-before-persistence policy.

  • src/control-plane/widget-instance-registry/widget-instance-registry.controller.ts -> @Get("schema")
PASS

Create preview contract

POST /api/admin/widget-instances/create-preview

auth: none

Backend can validate a future create payload without persisting widget_instances. Frontend may use this as deterministic QA before enabling direct save.

  • src/control-plane/widget-instance-registry/widget-instance-registry.controller.ts -> @Post("create-preview")
PASS

Update preview contract

PATCH /api/admin/widget-instances/update-preview

auth: none

Backend can validate a future update payload without persistence. This keeps Apply/preview/copy safe before audited save is enabled.

  • src/control-plane/widget-instance-registry/widget-instance-registry.controller.ts -> @Patch("update-preview")
PASS

Shared config validation preview

POST /api/admin/widget-instances/validate-config

auth: none

Backend validates shared widget config such as symbol, intervals, theme, locale, sizing, symbol URL, attribution and features.

  • src/control-plane/widget-instance-registry/widget-instance-registry.controller.ts -> @Post("validate-config")
PASS

Runtime config preview

POST /api/admin/widget-instances/runtime-config-preview

auth: none

Backend can preview runtime eligibility for the applied widget config before bootstrap and iframe runtime are involved.

  • src/control-plane/widget-instance-registry/widget-instance-registry.controller.ts -> @Post("runtime-config-preview")
PASS

Embed snippet preview

POST /api/admin/widget-instances/embed-snippet-preview

auth: none

Backend returns placeholder-based embed snippets and explicitly keeps runtime tokens out of copy-code output.

  • src/control-plane/widget-instance-registry/widget-instance-registry.controller.ts -> @Post("embed-snippet-preview")
WARNING

Generic persisted create endpoint

POST /api/admin/widget-instances

auth: admin-bearer

A generic admin CRUD endpoint exists, but builder direct save must stay disabled until authenticated admin flow, ownership guard, saved/applied semantics and audit evidence are wired in frontend.

  • src/control-plane/admin-api/admin-api.controller.ts -> @Post("widget-instances") + @ApiBearerAuth()
  • src/control-plane/admin-api/admin-api.service.ts -> permission widget_instances.write
WARNING

Generic persisted update endpoint

PATCH /api/admin/widget-instances/:id

auth: admin-bearer

A generic update endpoint exists for UUID-backed widget instances, but the current builder uses public widget codes for preview and must not guess persisted ids or collect credentials.

  • src/control-plane/admin-api/admin-api.controller.ts -> @Patch("widget-instances/:id([0-9a-fA-F-]{36})")
Frontend save request envelope

The request is dry-run only and carries the last applied builder draft. It is safe for validation/probe flows and does not contain runtime credentials.

{
  "schemaVersion": "kline.admin-widget-config.v1",
  "mode": "validate-config",
  "draft": {
    "widgetType": "advanced-kline-chart",
    "config": {
      "widgetId": "demo-advanced-kline",
      "widgetType": "advanced-kline-chart",
      "publicKey": "pk_demo_local_only",
      "symbol": {
        "id": "BINANCE:BTCUSDT",
        "ticker": "BINANCE:BTCUSDT",
        "displayName": "Bitcoin / Tether",
        "exchange": "BINANCE",
        "provider": "binance-public-demo",
        "pricePrecision": 2,
        "volumePrecision": 6,
        "currency": "USDT",
        "timezone": "UTC"
      },
      "interval": "1h",
      "allowedIntervals": [
        "1m",
        "5m",
        "15m",
        "30m",
        "1h",
        "4h",
        "1d"
      ],
      "theme": "dark",
      "locale": "fa",
      "features": {
        "toolbar": true,
        "indicators": true,
        "drawings": true,
        "realtime": true,
        "snapshot": true,
        "objectTree": true
      }
    },
    "chartStyle": "candle",
    "appliedAt": 1767268800000
  },
  "parentContainer": {
    "width": 1180,
    "minHeight": 620,
    "display": "block"
  },
  "dryRun": true,
  "requestedAt": 1784097188362
}
Backend preview payload

This is the shape the frontend can send to validate-config/runtime-config-preview once the backend is running. Persisted save is intentionally not called from this page.

{
  "tenantId": "tenant_frontend_preview",
  "license": {
    "id": "license_frontend_preview",
    "tenantId": "tenant_frontend_preview",
    "status": "active",
    "expiresAtMs": null,
    "revokedAtMs": null
  },
  "widgetType": {
    "id": "widget_type_advanced_chart",
    "code": "advanced_chart",
    "version": 1,
    "status": "active",
    "configSchemaJson": {
      "type": "object",
      "additionalProperties": true
    },
    "defaultConfigJson": {
      "symbol": "BINANCE:BTCUSDT",
      "symbols": [
        "BINANCE:BTCUSDT"
      ],
      "interval": "1h",
      "intervals": [
        "1m",
        "5m",
        "15m",
        "30m",
        "1h",
        "4h",
        "1d"
      ],
      "theme": "dark",
      "locale": "fa",
      "use_container_size": true,
      "transparent_background": false,
      "legal_branding": "backend_managed",
      "features": {
        "indicators": true,
        "drawings": true,
        "fullscreen": true,
        "realtime": true,
        "compare": false,
        "alerts": false,
        "events": false,
        "trading": false,
        "watchlist": false,
        "heatmapZoom": false,
        "screening": false,
        "calendar": false,
        "news": false,
        "technicalAnalysis": false,
        "fundamentals": false,
        "timeline": false
      }
    },
    "symbolRequirement": "single_required",
    "websiteEmbed": {
      "category": "website_embeddable_widgets",
      "mode": "iframe"
    }
  },
  "plan": {
    "maxWidgets": 10,
    "currentWidgetCount": 1,
    "maxSymbols": 20,
    "allowedWidgetTypes": [
      "advanced_chart",
      "advanced_chart@v1"
    ],
    "allowedSymbols": [
      "BINANCE:BTCUSDT"
    ],
    "allowedIntervals": [
      "1m",
      "5m",
      "15m",
      "30m",
      "1h",
      "4h",
      "1d"
    ],
    "featureFlags": {
      "indicators": true,
      "drawings": true,
      "fullscreen": true,
      "realtime": true,
      "compare": false,
      "alerts": false,
      "events": false,
      "trading": false,
      "watchlist": false,
      "heatmapZoom": false,
      "screening": false,
      "calendar": false,
      "news": false,
      "technicalAnalysis": false,
      "fundamentals": false,
      "timeline": false
    },
    "legalBrandingFeatureFlags": {
      "hideAttribution": false
    }
  },
  "name": "Advanced KLine Chart",
  "code": "demo-advanced-kline",
  "configJson": {
    "widget_type": "advanced-kline-chart",
    "symbol": "BINANCE:BTCUSDT",
    "symbols": [
      "BINANCE:BTCUSDT"
    ],
    "interval": "1h",
    "intervals": [
      "1m",
      "5m",
      "15m",
      "30m",
      "1h",
      "4h",
      "1d"
    ],
    "theme": "dark",
    "locale": "fa",
    "direction": "auto",
    "use_container_size": true,
    "transparent_background": false,
    "features": {
      "indicators": true,
      "drawings": true,
      "fullscreen": true,
      "realtime": true,
      "compare": false,
      "alerts": false,
      "events": false,
      "trading": false,
      "watchlist": false,
      "heatmapZoom": false,
      "screening": false,
      "calendar": false,
      "news": false,
      "technicalAnalysis": false,
      "fundamentals": false,
      "timeline": false
    },
    "klinecharts": {
      "chart_style": "candlestick",
      "frontend_widget_type": "advanced-kline-chart",
      "mvp_stage": "v1",
      "render_mode": "chart-runtime"
    }
  },
  "allowedSymbolsJson": [
    "BINANCE:BTCUSDT"
  ],
  "allowedIntervalsJson": [
    "1m",
    "5m",
    "15m",
    "30m",
    "1h",
    "4h",
    "1d"
  ],
  "locale": "fa",
  "runtimeFeaturesJson": {
    "indicators": true,
    "drawings": true,
    "fullscreen": true,
    "realtime": true,
    "compare": false,
    "alerts": false,
    "events": false,
    "trading": false,
    "watchlist": false,
    "heatmapZoom": false,
    "screening": false,
    "calendar": false,
    "news": false,
    "technicalAnalysis": false,
    "fundamentals": false,
    "timeline": false
  },
  "parentContainer": {
    "width": 1180,
    "minHeight": 620,
    "display": "block"
  }
}