chore: publish clean community baseline
This commit is contained in:
@@ -0,0 +1,394 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<base href="../">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Crank — Agents</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/variables.css">
|
||||
<link rel="stylesheet" href="css/layout.css">
|
||||
<link rel="stylesheet" href="css/pages.css">
|
||||
<link rel="stylesheet" href="css/catalog.css">
|
||||
<link rel="stylesheet" href="css/wizard.css">
|
||||
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
|
||||
</head>
|
||||
<body x-data="agents">
|
||||
|
||||
<!-- ═══════════════════ NAVBAR ═══════════════════ -->
|
||||
<nav class="navbar">
|
||||
<a href="/" class="nav-logo">
|
||||
<div class="nav-logo-mark"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="white"><path fill-rule="evenodd" d="M 10.000,1.800 L 12.287,3.962 L 15.408,3.557 L 15.987,6.650 L 18.750,8.157 L 17.400,11.000 L 18.750,13.843 L 15.987,15.350 L 15.408,18.443 L 12.287,18.038 L 10.000,20.200 L 7.713,18.038 L 4.592,18.443 L 4.013,15.350 L 1.250,13.843 L 2.600,11.000 L 1.250,8.157 L 4.013,6.650 L 4.592,3.557 L 7.713,3.962 Z M 12.800,11.000 A 2.8 2.8 0 1 0 7.200,11.000 A 2.8 2.8 0 1 0 12.800,11.000 Z"/><path fill-rule="evenodd" d="M 11.791,15.604 Q 16.083,17.179 21.375,21.154 A 2.00 2.00 0 1 1 23.625,17.846 Q 19.782,15.610 14.940,10.973 Z M 24.300,19.500 A 1.8 1.8 0 1 0 20.700,19.500 A 1.8 1.8 0 1 0 24.300,19.500 Z"/><path d="M 10.0,9.7 L 11.3,11.0 L 10.0,12.3 L 8.7,11.0 Z" fill="rgba(0,0,0,0.3)"/></svg></div>
|
||||
<span class="nav-logo-text">Crank</span>
|
||||
</a>
|
||||
|
||||
<!-- Workspace switcher -->
|
||||
<div class="ws-switcher" id="ws-switcher">
|
||||
<button class="ws-switcher-trigger" onclick="toggleWsSwitcher(event)">
|
||||
<div class="ws-dot" id="ws-dot">A</div>
|
||||
<span class="ws-current-name" id="ws-current-name">acme-workspace</span>
|
||||
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<div class="ws-dropdown" id="ws-dropdown" hidden>
|
||||
<div class="ws-dropdown-label" data-i18n="nav.workspaces">Your workspaces</div>
|
||||
<div id="ws-dropdown-list"></div>
|
||||
<div class="ws-dropdown-footer">
|
||||
<button class="ws-dropdown-create" onclick="window.location.href='/workspace-setup?mode=create'">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
|
||||
<span data-i18n="nav.create_workspace">Create workspace</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-links">
|
||||
<a href="/" class="nav-link" data-i18n="nav.operations">Operations</a>
|
||||
<a href="/agents" class="nav-link active" data-i18n="nav.agents">Agents</a>
|
||||
<a href="/api-keys" class="nav-link" data-i18n="nav.apikeys">API Keys</a>
|
||||
<a href="/secrets" class="nav-link" data-i18n="nav.secrets">Secrets</a>
|
||||
<a href="/logs" class="nav-link" data-i18n="nav.logs">Logs</a>
|
||||
<a href="/usage" class="nav-link" data-i18n="nav.usage">Usage</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-right">
|
||||
<button class="nav-hamburger" data-i18n-title="nav.menu" data-i18n-aria-label="nav.menu" aria-label="Menu"><span></span><span></span><span></span></button>
|
||||
<button class="nav-icon-btn" data-i18n-title="nav.notifications" title="Notifications">
|
||||
<svg width="15" height="15"><use href="icons/general/bell.svg#icon"/></svg>
|
||||
</button>
|
||||
<div class="nav-divider"></div>
|
||||
<div class="user-menu" @click.stop>
|
||||
<div class="nav-avatar" @click="openDropdown = openDropdown === 'user' ? null : 'user'" data-i18n-title="nav.account" title="Account">AT</div>
|
||||
<div class="user-dropdown" x-show="openDropdown === 'user'" x-transition style="display:none">
|
||||
<div class="user-dropdown-header">
|
||||
<div class="user-dropdown-name">Crank</div>
|
||||
<div class="user-dropdown-role" id="user-ws-role">—</div>
|
||||
</div>
|
||||
<button class="user-dropdown-item" onclick="window.location.href='/settings'">
|
||||
<svg width="13" height="13"><use href="icons/general/settings.svg#icon"/></svg>
|
||||
<span data-i18n="nav.settings">Settings</span>
|
||||
</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="user-dropdown-item danger" onclick="window.CrankAuth.logout()">
|
||||
<svg width="13" height="13"><use href="icons/general/logout.svg#icon"/></svg>
|
||||
<span data-i18n="nav.logout">Log out</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="mobile-nav" hidden>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.operations">Operations</a>
|
||||
<a href="/agents" class="mobile-nav-link active" data-i18n="nav.agents">Agents</a>
|
||||
<a href="/api-keys" class="mobile-nav-link" data-i18n="nav.apikeys">API Keys</a>
|
||||
<a href="/secrets" class="mobile-nav-link" data-i18n="nav.secrets">Secrets</a>
|
||||
<a href="/logs" class="mobile-nav-link" data-i18n="nav.logs">Logs</a>
|
||||
<a href="/usage" class="mobile-nav-link" data-i18n="nav.usage">Usage</a>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════ PAGE ═══════════════════ -->
|
||||
<div class="page">
|
||||
|
||||
<!-- Page header -->
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-heading" data-i18n="agents.title">Agents</h1>
|
||||
<p class="page-subheading" x-text="subtitleText()">Named MCP endpoints that expose a curated subset of operations to an LLM</p>
|
||||
</div>
|
||||
<button class="btn-new" @click="openCreate()">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
|
||||
<span data-i18n="agents.new">New agent</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="stats-row">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-i18n="agents.stats.total">Total agents</div>
|
||||
<div class="stat-value" x-text="agents.length">—</div>
|
||||
<div class="stat-delta" x-text="tKey('agents.stats.total_sub')">across this workspace</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-i18n="agents.stats.published">Published</div>
|
||||
<div class="stat-value" x-text="activeCount">—</div>
|
||||
<div class="stat-delta" x-text="agents.length ? tfKey('agents.stats.published_sub', { value: Math.round(activeCount/agents.length*100) }) : ''">—</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-i18n="agents.stats.tools">Operations exposed</div>
|
||||
<div class="stat-value" x-text="totalOpsExposed">—</div>
|
||||
<div class="stat-delta" x-text="tfKey('agents.stats.tools_sub', { count: agents.length })">—</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-i18n="agents.stats.calls">Calls today</div>
|
||||
<div class="stat-value" x-text="formatCalls(totalCalls)">—</div>
|
||||
<div class="stat-delta up" x-text="tKey('agents.stats.calls_sub')">all agents combined</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search + filter bar -->
|
||||
<div class="filter-bar">
|
||||
<div class="search-box">
|
||||
<svg width="14" height="14"><use href="icons/general/search.svg#icon"/></svg>
|
||||
<input
|
||||
type="text"
|
||||
data-i18n-ph="agents.search"
|
||||
placeholder="Search agents..."
|
||||
:value="agentSearch"
|
||||
@input="agentSearch = $event.target.value"
|
||||
>
|
||||
</div>
|
||||
<div class="filter-right">
|
||||
<span class="results-count" x-show="agentSearch">
|
||||
<span x-text="tfKey('agents.results', { shown: filteredAgents.length, total: agents.length })"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info callout -->
|
||||
<div class="agents-info-callout">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="var(--accent)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="7"/><path d="M8 11V8M8 5v-.5"/></svg>
|
||||
<span x-text="agentCalloutText()">Each agent gets its own MCP endpoint.</span>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div class="loading-spinner" x-show="loading">
|
||||
<div class="spinner"></div>
|
||||
<span data-i18n="agents.loading">Loading agents…</span>
|
||||
</div>
|
||||
|
||||
<div class="empty-state" x-show="!loading && loadError">
|
||||
<div class="empty-state-title" data-i18n="agents.error.title">Failed to load agents</div>
|
||||
<div class="empty-state-sub" x-text="loadError"></div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state: no agents at all -->
|
||||
<div class="empty-state" x-show="!loading && !loadError && agents.length === 0">
|
||||
<div class="empty-state-icon">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" style="opacity:0.35"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><path d="M14 17.5h7M17.5 14v7"/></svg>
|
||||
</div>
|
||||
<div class="empty-state-title" data-i18n="agents.empty.initial.title">No agents yet</div>
|
||||
<div class="empty-state-sub" x-text="emptyStateText()">Create your first agent to get a dedicated MCP endpoint with a curated set of tools.</div>
|
||||
<div class="empty-state-sub" style="margin-top:8px;" x-text="tKey('agents.empty.initial.next_step')">After creation, issue separate machine-access keys for this endpoint on the API Keys page.</div>
|
||||
<button class="btn-new" @click="openCreate()" style="margin-top: 16px;">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
|
||||
<span data-i18n="agents.new">New agent</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Empty state: search returned nothing -->
|
||||
<div class="empty-state" x-show="!loading && !loadError && agents.length > 0 && filteredAgents.length === 0">
|
||||
<div class="empty-state-title" x-text="tfKey('agents.empty.search.title', { query: agentSearch })">No agents match</div>
|
||||
<div class="empty-state-sub" data-i18n="agents.empty.search.sub">Try a different search term.</div>
|
||||
</div>
|
||||
|
||||
<!-- Agents grid -->
|
||||
<div class="agents-grid" x-show="!loading && !loadError && filteredAgents.length > 0">
|
||||
<template x-for="agent in filteredAgents" :key="agent.id">
|
||||
<div class="agent-card">
|
||||
|
||||
<!-- Card header -->
|
||||
<div class="agent-card-header">
|
||||
<div class="agent-card-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><path d="M14 17.5h7M17.5 14v7"/></svg>
|
||||
</div>
|
||||
<div class="agent-card-title-wrap">
|
||||
<div class="agent-card-name" x-text="agent.display_name"></div>
|
||||
<div class="agent-card-slug" x-text="agent.slug"></div>
|
||||
</div>
|
||||
<span :class="statusClass(agent.status)" x-text="lifecycleLabel(agent)"></span>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="agent-card-desc" x-text="agent.description"></div>
|
||||
|
||||
<!-- Stats row -->
|
||||
<div class="agent-card-stats">
|
||||
<div class="agent-stat">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 5h12M2 8h8M2 11h5"/></svg>
|
||||
<span class="agent-stat-value" x-text="agent.operation_count"></span>
|
||||
<span data-i18n="agents.card.tools">tools</span>
|
||||
</div>
|
||||
<div class="agent-stat">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M7 2H4a1 1 0 00-1 1v3M7 2l7 7M7 2v5h5"/><path d="M3 9v4a1 1 0 001 1h8a1 1 0 001-1V9"/></svg>
|
||||
<span class="agent-stat-value" x-text="agent.key_count"></span>
|
||||
<span data-i18n="agents.card.keys">API keys</span>
|
||||
</div>
|
||||
<div class="agent-stat" x-show="agent.calls_today > 0">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M1 8s3-5 7-5 7 5 7 5-3 5-7 5-7-5-7-5z"/><circle cx="8" cy="8" r="2"/></svg>
|
||||
<span class="agent-stat-value" x-text="formatCalls(agent.calls_today)"></span>
|
||||
<span data-i18n="agents.card.calls_today">calls today</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MCP endpoint -->
|
||||
<div class="agent-endpoint-row">
|
||||
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="var(--text-muted)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 8h12M10 4l4 4-4 4"/></svg>
|
||||
<code class="agent-endpoint-url" x-text="mcpEndpoint(agent)"></code>
|
||||
<button class="agent-endpoint-copy" @click.stop="copyEndpoint(agent)" :title="tKey('agents.card.copy_endpoint')">
|
||||
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="5" width="9" height="9" rx="1"/><path d="M5 5V3a1 1 0 011-1h7a1 1 0 011 1v7a1 1 0 01-1 1h-2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="agent-card-date" style="margin-top:8px;" x-text="endpointHelpText(agent)"></div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="agent-card-footer">
|
||||
<span class="agent-card-date" x-text="tfKey('agents.card.created', { date: formatDate(agent.created_at) })"></span>
|
||||
<div class="agent-card-actions">
|
||||
<button class="agent-action-btn" @click.stop="openEdit(agent)">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M11 2l3 3-8 8H3v-3l8-8z"/></svg>
|
||||
<span data-i18n="agents.action.edit">Edit</span>
|
||||
</button>
|
||||
<button class="agent-action-btn" @click.stop="applyLifecycle(agent, lifecycleAction(agent).key)">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 3.5v9l8-4.5-8-4.5z"/></svg>
|
||||
<span x-text="lifecycleAction(agent).label"></span>
|
||||
</button>
|
||||
<button class="agent-action-btn" x-show="agent.raw_status !== 'archived'" @click.stop="applyLifecycle(agent, 'archive')">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 4.5h11v2l-1 6.5h-8l-1-6.5v-2z"/><path d="M5 4.5v-1A1.5 1.5 0 016.5 2h3A1.5 1.5 0 0111 3.5v1"/></svg>
|
||||
<span data-i18n="agents.action.archive">Archive</span>
|
||||
</button>
|
||||
<button class="agent-action-btn danger" @click.stop="deleteAgent(agent.id)">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 4h12M5 4V3a1 1 0 011-1h4a1 1 0 011 1v1M10 8v4M6 8v4"/><path d="M3 4l1 9a1 1 0 001 1h6a1 1 0 001-1l1-9"/></svg>
|
||||
<span data-i18n="agents.action.delete">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</div><!-- /page -->
|
||||
|
||||
|
||||
<!-- ═══════════════════ DRAWER ═══════════════════ -->
|
||||
<!-- Overlay -->
|
||||
<div class="drawer-overlay" x-show="drawerOpen" x-transition:enter="transition-opacity" @click="closeDrawer()" style="display:none"></div>
|
||||
|
||||
<!-- Drawer panel -->
|
||||
<div class="drawer" :class="{ open: drawerOpen }" style="display:none" x-show="drawerOpen">
|
||||
|
||||
<!-- Drawer header -->
|
||||
<div class="drawer-header">
|
||||
<div>
|
||||
<div class="drawer-title" x-text="drawerMode === 'create' ? tKey('agents.drawer.new_title') : tKey('agents.drawer.edit_title')"></div>
|
||||
<div class="drawer-subtitle" x-text="drawerSubText()"></div>
|
||||
</div>
|
||||
<button class="drawer-close" @click="closeDrawer()">
|
||||
<svg width="14" height="14" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="1" y1="1" x2="11" y2="11"/><line x1="11" y1="1" x2="1" y2="11"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Drawer body -->
|
||||
<div class="drawer-body">
|
||||
|
||||
<!-- Identity section -->
|
||||
<div class="drawer-section">
|
||||
<div class="drawer-section-title" data-i18n="agents.drawer.identity">Identity</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 14px;">
|
||||
<label class="form-label"><span data-i18n="agents.drawer.display_name">Display name</span> <span class="form-label-required" data-i18n="agents.drawer.required">required</span></label>
|
||||
<input class="form-input" type="text" data-i18n-ph="agents.drawer.placeholder.name" placeholder="Customer Support" autocomplete="off"
|
||||
:value="form.display_name" @input="onNameInput($event.target.value)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 14px;">
|
||||
<label class="form-label"><span data-i18n="agents.drawer.slug">Slug</span> <span class="form-label-required" data-i18n="agents.drawer.required">required</span></label>
|
||||
<input class="form-input input-mono" type="text" data-i18n-ph="agents.drawer.placeholder.slug" placeholder="customer-support" autocomplete="off"
|
||||
:value="form.slug" @input="onSlugInput($event.target.value)">
|
||||
<div class="form-hint" x-show="form.slug">
|
||||
<span data-i18n="agents.drawer.endpoint">MCP endpoint</span>: <code style="font-family:monospace;font-size:11px;color:var(--accent)" x-text="'/mcp/v1/' + (localStorage.getItem('crank_workspace_slug') || 'default') + '/' + form.slug"></code>
|
||||
</div>
|
||||
<div class="form-hint" data-i18n="agents.drawer.slug_hint">Keep the slug stable after clients start using this endpoint. Changing it changes the MCP path.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 14px;">
|
||||
<label class="form-label"><span data-i18n="agents.drawer.description">Description</span> <span class="form-label-optional" data-i18n="agents.drawer.optional">(optional)</span></label>
|
||||
<textarea class="form-textarea" rows="3" data-i18n-ph="agents.drawer.placeholder.description" placeholder="What does this agent do? What LLM or use-case is it for?"
|
||||
x-model="form.description"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="agents.drawer.status">Status</label>
|
||||
<div class="agent-status-toggle">
|
||||
<button class="agent-status-opt" :class="{ active: form.status === 'published' }" @click="form.status = 'published'">
|
||||
<span class="status-dot" style="background: var(--success, #3fb950)"></span> <span data-i18n="agents.lifecycle.published">Published</span>
|
||||
</button>
|
||||
<button class="agent-status-opt" :class="{ active: form.status === 'draft' }" @click="form.status = 'draft'">
|
||||
<span class="status-dot" style="background: var(--text-muted)"></span> <span data-i18n="agents.lifecycle.draft">Draft</span>
|
||||
</button>
|
||||
<button class="agent-status-opt" :class="{ active: form.status === 'archived' }" @click="form.status = 'archived'">
|
||||
<span class="status-dot" style="background: var(--orange, #d29922)"></span> <span data-i18n="agents.lifecycle.archived">Archived</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Operations picker section -->
|
||||
<div class="drawer-section">
|
||||
<div class="drawer-section-header">
|
||||
<div class="drawer-section-title" data-i18n="agents.drawer.operations">Operations</div>
|
||||
<span class="drawer-section-count" x-text="tfKey('agents.drawer.selected', { count: form.selectedOps.length })"></span>
|
||||
</div>
|
||||
<div class="drawer-section-sub" x-text="operationsSubText()">Select which operations this agent exposes. LLMs connecting to this agent will only see these tools.</div>
|
||||
|
||||
<div class="ops-picker">
|
||||
<!-- Search -->
|
||||
<div class="ops-picker-search">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="var(--text-muted)" stroke-width="1.8" stroke-linecap="round"><circle cx="7" cy="7" r="5"/><path d="M12 12l2.5 2.5"/></svg>
|
||||
<input class="ops-picker-search-input" type="text" data-i18n-ph="agents.drawer.filter_ops" placeholder="Filter operations…" x-model="opSearch" autocomplete="off" spellcheck="false">
|
||||
<button class="ops-picker-clear" x-show="opSearch" @click="opSearch = ''" style="display:none">
|
||||
<svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="1" y1="1" x2="11" y2="11"/><line x1="11" y1="1" x2="1" y2="11"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- List -->
|
||||
<div class="ops-picker-list">
|
||||
<template x-for="op in filteredOps" :key="op.id || op.name">
|
||||
<div class="ops-picker-item" :class="{ selected: isOpSelected(op.id || op.name) }" @click="toggleOp(op.id || op.name)">
|
||||
<div class="ops-picker-check">
|
||||
<svg x-show="isOpSelected(op.id || op.name)" width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="var(--accent)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="1.5,6 4.5,9 10.5,3"/></svg>
|
||||
</div>
|
||||
<div class="ops-picker-info">
|
||||
<div class="ops-picker-name" x-text="op.display_name || op.name"></div>
|
||||
<div class="ops-picker-slug" x-text="op.name"></div>
|
||||
</div>
|
||||
<span :class="protocolBadge(op)" x-text="protocolLabel(op)" style="font-size:10px; padding: 2px 6px;"></span>
|
||||
<div class="ops-picker-status-dot" :class="'ops-status-' + op.status"></div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="ops-picker-empty" x-show="filteredOps.length === 0">
|
||||
<span x-text="tfKey('agents.drawer.ops_no_match', { query: opSearch })"></span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer count -->
|
||||
<div class="ops-picker-footer" x-show="form.selectedOps.length > 0">
|
||||
<span x-text="tfKey('agents.drawer.ops_selected', { count: form.selectedOps.length })"></span>
|
||||
<button @click="form.selectedOps = []" style="background:none;border:none;color:var(--text-muted);font-size:12px;cursor:pointer;margin-left:8px;" data-i18n="agents.drawer.clear_all">Clear all</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recommendation callout -->
|
||||
<div class="agents-rec-callout" x-show="form.selectedOps.length > 15">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><polygon points="8,1 15,14 1,14" fill="none"/><path d="M8 6v4M8 11.5v.5"/></svg>
|
||||
<span x-text="tfKey('agents.drawer.recommendation', { count: form.selectedOps.length })">You've selected tools.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /drawer-body -->
|
||||
|
||||
<!-- Drawer footer -->
|
||||
<div class="drawer-footer">
|
||||
<button class="btn-ghost-sm" style="padding: 8px 16px; font-size: 13px;" @click="closeDrawer()" data-i18n="agents.drawer.cancel">Cancel</button>
|
||||
<button class="btn-primary-sm" style="padding: 8px 20px; font-size: 13px;"
|
||||
:disabled="!form.display_name.trim() || !form.slug.trim()"
|
||||
@click="saveAgent()"
|
||||
x-text="drawerMode === 'create' ? tKey('agents.drawer.create') : tKey('agents.drawer.save')">
|
||||
Create agent
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div><!-- /drawer -->
|
||||
|
||||
|
||||
<script src="%CRANK_BUNDLE_AGENTS%"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,319 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<base href="../">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Crank — Agent Keys</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/variables.css">
|
||||
<link rel="stylesheet" href="css/layout.css">
|
||||
<link rel="stylesheet" href="css/pages.css">
|
||||
<style>
|
||||
.scope-checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
padding: 9px 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-canvas);
|
||||
}
|
||||
.scope-checkbox-name { font-size: 13px; font-weight: 500; color: var(--text-primary); }
|
||||
.scope-checkbox-desc { font-size: 11.5px; color: var(--text-muted); }
|
||||
.keys-card-list { display: none; }
|
||||
@media (max-width: 720px) {
|
||||
#keys-table-wrap { display: none; }
|
||||
.keys-card-list { display: grid; }
|
||||
}
|
||||
</style>
|
||||
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ═══════════════════ NAVBAR ═══════════════════ -->
|
||||
<nav class="navbar">
|
||||
<a href="/" class="nav-logo">
|
||||
<div class="nav-logo-mark"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="white"><path fill-rule="evenodd" d="M 10.000,1.800 L 12.287,3.962 L 15.408,3.557 L 15.987,6.650 L 18.750,8.157 L 17.400,11.000 L 18.750,13.843 L 15.987,15.350 L 15.408,18.443 L 12.287,18.038 L 10.000,20.200 L 7.713,18.038 L 4.592,18.443 L 4.013,15.350 L 1.250,13.843 L 2.600,11.000 L 1.250,8.157 L 4.013,6.650 L 4.592,3.557 L 7.713,3.962 Z M 12.800,11.000 A 2.8 2.8 0 1 0 7.200,11.000 A 2.8 2.8 0 1 0 12.800,11.000 Z"/><path fill-rule="evenodd" d="M 11.791,15.604 Q 16.083,17.179 21.375,21.154 A 2.00 2.00 0 1 1 23.625,17.846 Q 19.782,15.610 14.940,10.973 Z M 24.300,19.500 A 1.8 1.8 0 1 0 20.700,19.500 A 1.8 1.8 0 1 0 24.300,19.500 Z"/><path d="M 10.0,9.7 L 11.3,11.0 L 10.0,12.3 L 8.7,11.0 Z" fill="rgba(0,0,0,0.3)"/></svg></div>
|
||||
<span class="nav-logo-text">Crank</span>
|
||||
</a>
|
||||
|
||||
<!-- Workspace switcher -->
|
||||
<div class="ws-switcher" id="ws-switcher">
|
||||
<button class="ws-switcher-trigger" onclick="toggleWsSwitcher(event)">
|
||||
<div class="ws-dot" id="ws-dot">A</div>
|
||||
<span class="ws-current-name" id="ws-current-name">acme-workspace</span>
|
||||
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<div class="ws-dropdown" id="ws-dropdown" hidden>
|
||||
<div class="ws-dropdown-label" data-i18n="nav.workspaces">Your workspaces</div>
|
||||
<div id="ws-dropdown-list"></div>
|
||||
<div class="ws-dropdown-footer">
|
||||
<button class="ws-dropdown-create" onclick="window.location.href='/workspace-setup?mode=create'">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
|
||||
<span data-i18n="nav.create_workspace">Create workspace</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-links">
|
||||
<a href="/" class="nav-link" data-i18n="nav.operations">Operations</a>
|
||||
<a href="/agents" class="nav-link" data-i18n="nav.agents">Agents</a>
|
||||
<a href="/api-keys" class="nav-link active" data-i18n="nav.apikeys">API Keys</a>
|
||||
<a href="/secrets" class="nav-link" data-i18n="nav.secrets">Secrets</a>
|
||||
<a href="/logs" class="nav-link" data-i18n="nav.logs">Logs</a>
|
||||
<a href="/usage" class="nav-link" data-i18n="nav.usage">Usage</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-right">
|
||||
<button class="nav-hamburger" data-i18n-title="nav.menu" data-i18n-aria-label="nav.menu" aria-label="Menu"><span></span><span></span><span></span></button>
|
||||
|
||||
|
||||
<button class="nav-icon-btn" data-i18n-title="nav.notifications" title="Notifications">
|
||||
<svg width="15" height="15"><use href="icons/general/bell.svg#icon"/></svg>
|
||||
</button>
|
||||
|
||||
<div class="nav-divider"></div>
|
||||
|
||||
<div class="user-menu">
|
||||
<div class="nav-avatar" data-i18n-title="nav.account" title="Account">AT</div>
|
||||
<div class="user-dropdown">
|
||||
<div class="user-dropdown-header">
|
||||
<div class="user-dropdown-name">Crank</div>
|
||||
<div class="user-dropdown-role">—</div>
|
||||
</div>
|
||||
<button class="user-dropdown-item" data-action="settings-link">
|
||||
<svg width="13" height="13"><use href="icons/general/settings.svg#icon"/></svg>
|
||||
<span data-i18n="nav.settings">Settings</span>
|
||||
</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="user-dropdown-item danger" data-action="logout">
|
||||
<svg width="13" height="13"><use href="icons/general/logout.svg#icon"/></svg>
|
||||
<span data-i18n="nav.logout">Log out</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="mobile-nav" hidden>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.operations">Operations</a>
|
||||
<a href="/agents" class="mobile-nav-link" data-i18n="nav.agents">Agents</a>
|
||||
<a href="/api-keys" class="mobile-nav-link active" data-i18n="nav.apikeys">API Keys</a>
|
||||
<a href="/secrets" class="mobile-nav-link" data-i18n="nav.secrets">Secrets</a>
|
||||
<a href="/logs" class="mobile-nav-link" data-i18n="nav.logs">Logs</a>
|
||||
<a href="/usage" class="mobile-nav-link" data-i18n="nav.usage">Usage</a>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════ PAGE ═══════════════════ -->
|
||||
<div class="page">
|
||||
|
||||
<div class="page-header">
|
||||
<div class="page-header-text">
|
||||
<h1 class="page-title" data-i18n="apikeys.title">Agent Keys</h1>
|
||||
<p class="page-subtitle" data-i18n="apikeys.subtitle">Keys authenticate external MCP clients against a specific AI agent endpoint.</p>
|
||||
</div>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-primary" id="btn-create-key" type="button">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="currentColor"><path d="M7.75 2a.75.75 0 01.75.75V7h4.25a.75.75 0 010 1.5H8.5v4.25a.75.75 0 01-1.5 0V8.5H2.75a.75.75 0 010-1.5H7V2.75A.75.75 0 017.75 2z"/></svg>
|
||||
<span data-i18n="apikeys.new">Create key</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="callout info">
|
||||
<svg class="callout-icon" width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="var(--blue)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="8" cy="8" r="6.5"/>
|
||||
<path d="M8 11V8M8 5.5V5"/>
|
||||
</svg>
|
||||
<div>
|
||||
<strong data-i18n="apikeys.callout.title">Keys are only shown once.</strong>
|
||||
<span data-i18n="apikeys.callout.body">Copy and store them securely immediately after creation — Crank stores only a hash. `Last used` updates after successful MCP authentication.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="section-card-header">
|
||||
<div>
|
||||
<div class="section-card-title" data-i18n="apikeys.agent.title">Agent access</div>
|
||||
<div class="section-card-subtitle" id="agent-access-subtitle" data-i18n="apikeys.agent.subtitle">Select the AI agent whose MCP endpoint should accept this key.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-card-body" style="display:grid;gap:12px;">
|
||||
<div class="field-group" style="margin-bottom:0;">
|
||||
<label class="field-label" for="agent-select" data-i18n="apikeys.agent.label">AI agent</label>
|
||||
<select class="field-input" id="agent-select"></select>
|
||||
<div class="field-hint" id="agent-endpoint-hint" data-testid="agent-key-endpoint-hint"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="section-card-header">
|
||||
<div>
|
||||
<div class="section-card-title" id="machine-access-title" data-i18n="apikeys.machine_access.title">Machine access modes</div>
|
||||
<div class="section-card-subtitle" id="machine-access-subtitle" data-i18n="apikeys.machine_access.subtitle">This build exposes the stable machine-access contract and shows which modes are available now.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-card-body" style="display:grid;gap:10px;">
|
||||
<div style="font-size:13px;color:var(--text-secondary);line-height:1.65;" id="machine-access-summary" data-testid="machine-access-summary">
|
||||
Community currently supports static AI-agent keys.
|
||||
</div>
|
||||
<div class="callout info">
|
||||
<svg class="callout-icon" width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="var(--blue)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="8" cy="8" r="6.5"/>
|
||||
<path d="M8 11V8M8 5.5V5"/>
|
||||
</svg>
|
||||
<div id="machine-access-note" data-i18n="apikeys.machine_access.note">Short-lived and one-time token issuance uses the same public HTTP surface, but requires a commercial edition.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="section-card-header">
|
||||
<div>
|
||||
<div class="section-card-title" data-i18n="apikeys.active.title">Active keys</div>
|
||||
<div class="section-card-subtitle" id="keys-summary-subtitle">3 active · 1 revoked</div>
|
||||
</div>
|
||||
<div class="filter-bar-search" style="max-width: 220px; margin: 0;">
|
||||
<svg width="13" height="13" style="position:absolute; left:9px; top:50%; transform:translateY(-50%); color:var(--text-muted);" viewBox="0 0 16 16" fill="currentColor"><path d="M10.68 11.74a6 6 0 01-7.922-8.982 6 6 0 018.982 7.922l3.04 3.04a.749.749 0 01-.326 1.275.749.749 0 01-.734-.215zM11.5 7a4.499 4.499 0 11-8.997 0A4.499 4.499 0 0111.5 7z"/></svg>
|
||||
<input type="text" id="key-search" data-i18n-ph="apikeys.search" placeholder="Filter keys…" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="keys-table-wrap">
|
||||
<table class="data-table" id="keys-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="apikeys.th.name">Name</th>
|
||||
<th data-i18n="apikeys.th.prefix">Key prefix</th>
|
||||
<th data-i18n="apikeys.th.scopes">Scopes</th>
|
||||
<th data-i18n="apikeys.th.created">Created</th>
|
||||
<th data-i18n="apikeys.th.last">Last used</th>
|
||||
<th data-i18n="apikeys.th.status">Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="keys-tbody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="resource-list keys-card-list" id="keys-card-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="section-card-header">
|
||||
<div class="section-card-title" data-i18n="apikeys.scope_ref">Scope reference</div>
|
||||
</div>
|
||||
<div class="section-card-body" style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;">
|
||||
<div>
|
||||
<div style="font-size:12.5px;font-weight:600;color:var(--text-primary);margin-bottom:4px;display:flex;align-items:center;gap:6px;">
|
||||
<span class="badge badge-scope">read</span>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--text-muted);line-height:1.55;" data-i18n="apikeys.scope.read">Initialize MCP sessions, ping the server, and list tools for a workspace agent.</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:12.5px;font-weight:600;color:var(--text-primary);margin-bottom:4px;">
|
||||
<span class="badge badge-scope">write</span>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--text-muted);line-height:1.55;" data-i18n="apikeys.scope.write">Execute `tools/call` requests against published agent toolsets.</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:12.5px;font-weight:600;color:var(--text-primary);margin-bottom:4px;">
|
||||
<span class="badge badge-scope">deploy</span>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--text-muted);line-height:1.55;" data-i18n="apikeys.scope.deploy">Reserved for deploy-scoped automation. Today it also permits MCP read/write flows.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /page -->
|
||||
|
||||
<!-- ══ Create Key Modal ══ -->
|
||||
<div class="modal-overlay" id="modal-create">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title" data-i18n="apikeys.modal.title">Create agent key</span>
|
||||
<button class="modal-close" id="modal-close-btn" type="button">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
|
||||
<line x1="1" y1="1" x2="11" y2="11"/><line x1="11" y1="1" x2="1" y2="11"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" id="modal-form-body">
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="apikeys.modal.name">Key name</label>
|
||||
<input class="field-input" id="new-key-name" type="text" data-i18n-ph="apikeys.modal.name_placeholder" placeholder="e.g. Production, CI pipeline" autocomplete="off">
|
||||
<div class="field-hint" data-i18n="apikeys.modal.name_hint">A descriptive label to identify the key. Only visible to admins.</div>
|
||||
</div>
|
||||
<div class="field-group" style="margin-bottom:0;">
|
||||
<label class="field-label" data-i18n="apikeys.modal.scopes">Scopes</label>
|
||||
<div style="display:flex;flex-direction:column;gap:8px;margin-top:2px;" id="scope-checkboxes">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body" id="modal-reveal-body" hidden>
|
||||
<div class="callout warning" style="margin-bottom:16px;">
|
||||
<svg class="callout-icon" width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="var(--amber)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="8,1.5 15.5,14.5 0.5,14.5" fill="none"/><path d="M8 6v4M8 11.5v.5"/>
|
||||
</svg>
|
||||
<div><strong data-i18n="apikeys.modal.reveal_title">Copy this key now.</strong> <span data-i18n="apikeys.modal.reveal_body">It won't be shown again.</span></div>
|
||||
</div>
|
||||
<div class="key-reveal">
|
||||
<span class="key-mask" id="reveal-key-value">mcp_prod_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</span>
|
||||
<button class="key-copy" id="copy-key-btn" type="button" data-i18n-title="apikeys.modal.copy" title="Copy to clipboard">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="5" y="5" width="9" height="9" rx="1.5"/>
|
||||
<path d="M11 5V3.5A1.5 1.5 0 009.5 2H3.5A1.5 1.5 0 002 3.5v6A1.5 1.5 0 003.5 11H5"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" id="modal-footer-create">
|
||||
<button class="btn-secondary" id="modal-cancel-btn" type="button" data-i18n="apikeys.modal.cancel">Cancel</button>
|
||||
<button class="btn-primary" id="modal-confirm-btn" type="button" data-i18n="apikeys.modal.create">Create key</button>
|
||||
</div>
|
||||
<div class="modal-footer" id="modal-footer-done" hidden>
|
||||
<button class="btn-primary" id="modal-done-btn" type="button" data-i18n="apikeys.modal.done">Done — I've copied the key</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Template: scope checkbox row -->
|
||||
<template id="tmpl-scope-checkbox">
|
||||
<label class="scope-checkbox-label">
|
||||
<input type="checkbox" data-scope="" style="width:14px;height:14px;accent-color:var(--accent);cursor:pointer;flex-shrink:0;">
|
||||
<div>
|
||||
<div class="scope-checkbox-name"></div>
|
||||
<div class="scope-checkbox-desc"></div>
|
||||
</div>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<!-- Template: API key table row -->
|
||||
<template id="tmpl-key-row">
|
||||
<tr>
|
||||
<td class="col-name"></td>
|
||||
<td class="col-mono"></td>
|
||||
<td><div class="col-scopes" style="display:flex;gap:4px;flex-wrap:wrap;"></div></td>
|
||||
<td class="col-created"></td>
|
||||
<td class="col-last-used"></td>
|
||||
<td class="col-status"></td>
|
||||
<td class="col-actions">
|
||||
<span class="actions-active">
|
||||
<button class="btn-icon" data-i18n-title="apikeys.action.copy_prefix" title="Copy key prefix"><svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="5" width="9" height="9" rx="1.5"/><path d="M11 5V3.5A1.5 1.5 0 009.5 2H3.5A1.5 1.5 0 002 3.5v6A1.5 1.5 0 003.5 11H5"/></svg></button>
|
||||
<button class="btn-icon danger" data-i18n-title="apikeys.action.revoke" title="Revoke key"><svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="6.5"/><path d="M5.5 10.5l5-5M10.5 10.5l-5-5"/></svg></button>
|
||||
</span>
|
||||
<span class="actions-revoked" hidden>
|
||||
<button class="btn-icon" data-i18n-title="apikeys.action.delete" title="Delete"><svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2h4M2.5 4h11M5 4l.5 9h5l.5-9"/></svg></button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script src="%CRANK_BUNDLE_API_KEYS%"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
<div class="field-group" style="margin-top:8px;">
|
||||
<label class="field-label">Interface language</label>
|
||||
<div class="lang-switcher" style="display:flex;gap:6px;margin-top:6px;">
|
||||
<button class="lang-btn" data-lang="en" onclick="setLang('en')">
|
||||
<span class="lang-flag">🇬🇧</span>
|
||||
<span data-i18n="settings.lang.en">English</span>
|
||||
</button>
|
||||
<button class="lang-btn" data-lang="ru" onclick="setLang('ru')">
|
||||
<span class="lang-flag">🇷🇺</span>
|
||||
<span data-i18n="settings.lang.ru">Русский</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<base href="../">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Crank — Sign in</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/variables.css">
|
||||
<link rel="stylesheet" href="css/login.css">
|
||||
<script src="%CRANK_BUNDLE_LOGIN%"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="login-page">
|
||||
<div class="login-card">
|
||||
|
||||
<div class="login-logo">
|
||||
<div class="login-logo-mark"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" width="22" height="22" fill="white"><path fill-rule="evenodd" d="M 10.000,1.800 L 12.287,3.962 L 15.408,3.557 L 15.987,6.650 L 18.750,8.157 L 17.400,11.000 L 18.750,13.843 L 15.987,15.350 L 15.408,18.443 L 12.287,18.038 L 10.000,20.200 L 7.713,18.038 L 4.592,18.443 L 4.013,15.350 L 1.250,13.843 L 2.600,11.000 L 1.250,8.157 L 4.013,6.650 L 4.592,3.557 L 7.713,3.962 Z M 12.800,11.000 A 2.8 2.8 0 1 0 7.200,11.000 A 2.8 2.8 0 1 0 12.800,11.000 Z"/><path fill-rule="evenodd" d="M 11.791,15.604 Q 16.083,17.179 21.375,21.154 A 2.00 2.00 0 1 1 23.625,17.846 Q 19.782,15.610 14.940,10.973 Z M 24.300,19.500 A 1.8 1.8 0 1 0 20.700,19.500 A 1.8 1.8 0 1 0 24.300,19.500 Z"/><path d="M 10.0,9.7 L 11.3,11.0 L 10.0,12.3 L 8.7,11.0 Z" fill="rgba(0,0,0,0.3)"/></svg></div>
|
||||
<span class="login-logo-text">Crank</span>
|
||||
</div>
|
||||
|
||||
<div class="login-box">
|
||||
<div class="login-heading" data-i18n="login.title">Sign in</div>
|
||||
<div class="login-sub" data-i18n="login.subtitle">Welcome back to your workspace</div>
|
||||
<div class="login-note" data-i18n="login.coming_soon">Password reset and SSO will be added later.</div>
|
||||
|
||||
<div class="login-error" id="login-error" data-i18n="login.error.invalid">
|
||||
Invalid email or password. Please try again.
|
||||
</div>
|
||||
|
||||
<form id="login-form" novalidate>
|
||||
<div class="field">
|
||||
<label class="field-label" for="email" data-i18n="login.email_label">Email address</label>
|
||||
<input class="field-input" type="email" id="email" data-i18n-ph="login.email_placeholder" placeholder="you@acme.com" autocomplete="email" autofocus>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="field-label" for="password" data-i18n="login.password">Password</label>
|
||||
<input class="field-input" type="password" id="password" placeholder="••••••••" autocomplete="current-password">
|
||||
</div>
|
||||
|
||||
<button class="btn-signin" type="submit" data-i18n="login.submit">Sign in</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="version-badge">Crank v0.9.0-beta</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,142 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<base href="../">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Crank — Logs</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/variables.css">
|
||||
<link rel="stylesheet" href="css/layout.css">
|
||||
<link rel="stylesheet" href="css/pages.css">
|
||||
<link rel="stylesheet" href="css/logs.css">
|
||||
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar">
|
||||
<a href="/" class="nav-logo">
|
||||
<div class="nav-logo-mark"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="white"><path fill-rule="evenodd" d="M 10.000,1.800 L 12.287,3.962 L 15.408,3.557 L 15.987,6.650 L 18.750,8.157 L 17.400,11.000 L 18.750,13.843 L 15.987,15.350 L 15.408,18.443 L 12.287,18.038 L 10.000,20.200 L 7.713,18.038 L 4.592,18.443 L 4.013,15.350 L 1.250,13.843 L 2.600,11.000 L 1.250,8.157 L 4.013,6.650 L 4.592,3.557 L 7.713,3.962 Z M 12.800,11.000 A 2.8 2.8 0 1 0 7.200,11.000 A 2.8 2.8 0 1 0 12.800,11.000 Z"/><path fill-rule="evenodd" d="M 11.791,15.604 Q 16.083,17.179 21.375,21.154 A 2.00 2.00 0 1 1 23.625,17.846 Q 19.782,15.610 14.940,10.973 Z M 24.300,19.500 A 1.8 1.8 0 1 0 20.700,19.500 A 1.8 1.8 0 1 0 24.300,19.500 Z"/><path d="M 10.0,9.7 L 11.3,11.0 L 10.0,12.3 L 8.7,11.0 Z" fill="rgba(0,0,0,0.3)"/></svg></div>
|
||||
<span class="nav-logo-text">Crank</span>
|
||||
</a>
|
||||
|
||||
<!-- Workspace switcher -->
|
||||
<div class="ws-switcher" id="ws-switcher">
|
||||
<button class="ws-switcher-trigger" onclick="toggleWsSwitcher(event)">
|
||||
<div class="ws-dot" id="ws-dot">A</div>
|
||||
<span class="ws-current-name" id="ws-current-name">acme-workspace</span>
|
||||
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<div class="ws-dropdown" id="ws-dropdown" hidden>
|
||||
<div class="ws-dropdown-label" data-i18n="nav.workspaces">Your workspaces</div>
|
||||
<div id="ws-dropdown-list"></div>
|
||||
<div class="ws-dropdown-footer">
|
||||
<button class="ws-dropdown-create" onclick="window.location.href='/workspace-setup?mode=create'">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
|
||||
<span data-i18n="nav.create_workspace">Create workspace</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-links">
|
||||
<a href="/" class="nav-link" data-i18n="nav.operations">Operations</a>
|
||||
<a href="/agents" class="nav-link" data-i18n="nav.agents">Agents</a>
|
||||
<a href="/api-keys" class="nav-link" data-i18n="nav.apikeys">API Keys</a>
|
||||
<a href="/secrets" class="nav-link" data-i18n="nav.secrets">Secrets</a>
|
||||
<a href="/logs" class="nav-link active" data-i18n="nav.logs">Logs</a>
|
||||
<a href="/usage" class="nav-link" data-i18n="nav.usage">Usage</a>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<button class="nav-hamburger" data-i18n-title="nav.menu" data-i18n-aria-label="nav.menu" aria-label="Menu"><span></span><span></span><span></span></button>
|
||||
<button class="nav-icon-btn" data-i18n-title="nav.notifications" title="Notifications">
|
||||
<svg width="15" height="15"><use href="icons/general/bell.svg#icon"/></svg>
|
||||
</button>
|
||||
<div class="nav-divider"></div>
|
||||
<div class="user-menu">
|
||||
<div class="nav-avatar" data-i18n-title="nav.account" title="Account">AT</div>
|
||||
<div class="user-dropdown">
|
||||
<div class="user-dropdown-header">
|
||||
<div class="user-dropdown-name">Crank</div>
|
||||
<div class="user-dropdown-role">—</div>
|
||||
</div>
|
||||
<button class="user-dropdown-item" data-action="settings-link">
|
||||
<svg width="13" height="13"><use href="icons/general/settings.svg#icon"/></svg>
|
||||
<span data-i18n="nav.settings">Settings</span>
|
||||
</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="user-dropdown-item danger" data-action="logout">
|
||||
<svg width="13" height="13"><use href="icons/general/logout.svg#icon"/></svg>
|
||||
<span data-i18n="nav.logout">Log out</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="mobile-nav" hidden>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.operations">Operations</a>
|
||||
<a href="/agents" class="mobile-nav-link" data-i18n="nav.agents">Agents</a>
|
||||
<a href="/api-keys" class="mobile-nav-link" data-i18n="nav.apikeys">API Keys</a>
|
||||
<a href="/secrets" class="mobile-nav-link" data-i18n="nav.secrets">Secrets</a>
|
||||
<a href="/logs" class="mobile-nav-link active" data-i18n="nav.logs">Logs</a>
|
||||
<a href="/usage" class="mobile-nav-link" data-i18n="nav.usage">Usage</a>
|
||||
</div>
|
||||
|
||||
<div class="page">
|
||||
|
||||
<div class="page-header">
|
||||
<div class="page-header-text">
|
||||
<h1 class="page-title" data-i18n="logs.title">Logs</h1>
|
||||
<p class="page-subtitle" data-i18n="logs.subtitle">Real-time invocation log for all operations in this workspace.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="log-toolbar">
|
||||
<div class="live-dot"></div>
|
||||
<span class="live-label" data-i18n="logs.live">Live</span>
|
||||
<div class="toolbar-sep"></div>
|
||||
|
||||
<select class="time-range-select" id="time-range">
|
||||
<option value="30m" data-i18n="logs.range.30m">Last 30 min</option>
|
||||
<option value="1h" selected data-i18n="logs.range.1h">Last 1 hour</option>
|
||||
<option value="6h" data-i18n="logs.range.6h">Last 6 hours</option>
|
||||
<option value="24h" data-i18n="logs.range.24h">Last 24 hours</option>
|
||||
<option value="7d" data-i18n="logs.range.7d">Last 7 days</option>
|
||||
</select>
|
||||
|
||||
<div class="toolbar-sep"></div>
|
||||
|
||||
<button class="filter-chip active" data-level="all" id="chip-all" data-i18n="logs.level.all">All</button>
|
||||
<button class="filter-chip" data-level="info" id="chip-info">
|
||||
<span class="log-level info" style="padding:0 4px;font-size:10px;">INFO</span>
|
||||
</button>
|
||||
<button class="filter-chip" data-level="warn" id="chip-warn">
|
||||
<span class="log-level warn" style="padding:0 4px;font-size:10px;">WARN</span>
|
||||
</button>
|
||||
<button class="filter-chip" data-level="error" id="chip-error">
|
||||
<span class="log-level error" style="padding:0 4px;font-size:10px;">ERROR</span>
|
||||
</button>
|
||||
|
||||
<div class="filter-bar-search" style="max-width:240px;margin:0;">
|
||||
<svg width="13" height="13" style="position:absolute;left:9px;top:50%;transform:translateY(-50%);color:var(--text-muted);" viewBox="0 0 16 16" fill="currentColor"><path d="M10.68 11.74a6 6 0 01-7.922-8.982 6 6 0 018.982 7.922l3.04 3.04a.749.749 0 01-.326 1.275.749.749 0 01-.734-.215zM11.5 7a4.499 4.499 0 11-8.997 0A4.499 4.499 0 0111.5 7z"/></svg>
|
||||
<input type="text" id="log-search" data-i18n-ph="logs.search_messages" placeholder="Search messages…" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<button class="refresh-btn" id="refresh-btn" type="button">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M1.705 8.005a.75.75 0 01.834.656 5.5 5.5 0 009.592 2.97l-1.204-1.204a.25.25 0 01.177-.427h3.646a.25.25 0 01.25.25v3.646a.25.25 0 01-.427.177l-1.38-1.38A7.001 7.001 0 011.05 8.84a.75.75 0 01.656-.834zM8 2.5a5.487 5.487 0 00-4.131 1.869l1.204 1.204A.25.25 0 014.896 6H1.25A.25.25 0 011 5.75V2.104a.25.25 0 01.427-.177l1.38 1.38A7.001 7.001 0 0114.95 7.16a.75.75 0 01-1.49.178A5.501 5.501 0 008 2.5z"/></svg>
|
||||
<span data-i18n="logs.refresh">Refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="log-list" id="log-list">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="%CRANK_BUNDLE_LOGS%"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,242 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<base href="../">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Crank — Secrets</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/variables.css">
|
||||
<link rel="stylesheet" href="css/layout.css">
|
||||
<link rel="stylesheet" href="css/pages.css">
|
||||
<style>
|
||||
.secret-value-grid {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
.secret-inline-note {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.55;
|
||||
}
|
||||
.secret-ref-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.secret-ref-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 8px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-canvas);
|
||||
color: var(--text-secondary);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
.secret-ref-pill strong {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.secrets-card-list { display: none; }
|
||||
@media (max-width: 720px) {
|
||||
#secrets-table-wrap { display: none; }
|
||||
.secrets-card-list { display: grid; }
|
||||
}
|
||||
</style>
|
||||
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<a href="/" class="nav-logo">
|
||||
<div class="nav-logo-mark"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="white"><path fill-rule="evenodd" d="M 10.000,1.800 L 12.287,3.962 L 15.408,3.557 L 15.987,6.650 L 18.750,8.157 L 17.400,11.000 L 18.750,13.843 L 15.987,15.350 L 15.408,18.443 L 12.287,18.038 L 10.000,20.200 L 7.713,18.038 L 4.592,18.443 L 4.013,15.350 L 1.250,13.843 L 2.600,11.000 L 1.250,8.157 L 4.013,6.650 L 4.592,3.557 L 7.713,3.962 Z M 12.800,11.000 A 2.8 2.8 0 1 0 7.200,11.000 A 2.8 2.8 0 1 0 12.800,11.000 Z"/><path fill-rule="evenodd" d="M 11.791,15.604 Q 16.083,17.179 21.375,21.154 A 2.00 2.00 0 1 1 23.625,17.846 Q 19.782,15.610 14.940,10.973 Z M 24.300,19.500 A 1.8 1.8 0 1 0 20.700,19.500 A 1.8 1.8 0 1 0 24.300,19.500 Z"/><path d="M 10.0,9.7 L 11.3,11.0 L 10.0,12.3 L 8.7,11.0 Z" fill="rgba(0,0,0,0.3)"/></svg></div>
|
||||
<span class="nav-logo-text">Crank</span>
|
||||
</a>
|
||||
|
||||
<div class="ws-switcher" id="ws-switcher">
|
||||
<button class="ws-switcher-trigger" onclick="toggleWsSwitcher(event)">
|
||||
<div class="ws-dot" id="ws-dot">A</div>
|
||||
<span class="ws-current-name" id="ws-current-name" data-testid="shell-workspace-name">workspace</span>
|
||||
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<div class="ws-dropdown" id="ws-dropdown" hidden>
|
||||
<div class="ws-dropdown-label" data-i18n="nav.workspaces">Your workspaces</div>
|
||||
<div id="ws-dropdown-list"></div>
|
||||
<div class="ws-dropdown-footer">
|
||||
<button class="ws-dropdown-create" onclick="window.location.href='/workspace-setup?mode=create'">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
|
||||
<span data-i18n="nav.create_workspace">Create workspace</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-links">
|
||||
<a href="/" class="nav-link" data-i18n="nav.operations">Operations</a>
|
||||
<a href="/agents" class="nav-link" data-i18n="nav.agents">Agents</a>
|
||||
<a href="/api-keys" class="nav-link" data-i18n="nav.apikeys">API Keys</a>
|
||||
<a href="/secrets" class="nav-link active" data-i18n="nav.secrets">Secrets</a>
|
||||
<a href="/logs" class="nav-link" data-i18n="nav.logs">Logs</a>
|
||||
<a href="/usage" class="nav-link" data-i18n="nav.usage">Usage</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-right">
|
||||
<button class="nav-hamburger" data-i18n-title="nav.menu" data-i18n-aria-label="nav.menu" aria-label="Menu"><span></span><span></span><span></span></button>
|
||||
<button class="nav-icon-btn" data-i18n-title="nav.notifications" title="Notifications"><svg width="15" height="15"><use href="icons/general/bell.svg#icon"/></svg></button>
|
||||
<div class="nav-divider"></div>
|
||||
<div class="user-menu">
|
||||
<div class="nav-avatar" data-testid="shell-avatar" data-i18n-title="nav.account" title="Account">AT</div>
|
||||
<div class="user-dropdown">
|
||||
<div class="user-dropdown-header"><div class="user-dropdown-name" data-testid="shell-user-name">Crank</div><div class="user-dropdown-role" data-testid="shell-user-role">—</div></div>
|
||||
<button class="user-dropdown-item" data-action="settings-link"><svg width="13" height="13"><use href="icons/general/settings.svg#icon"/></svg><span data-i18n="nav.settings">Settings</span></button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="user-dropdown-item danger" data-action="logout"><svg width="13" height="13"><use href="icons/general/logout.svg#icon"/></svg><span data-i18n="nav.logout">Log out</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="mobile-nav" hidden>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.operations">Operations</a>
|
||||
<a href="/agents" class="mobile-nav-link" data-i18n="nav.agents">Agents</a>
|
||||
<a href="/api-keys" class="mobile-nav-link" data-i18n="nav.apikeys">API Keys</a>
|
||||
<a href="/secrets" class="mobile-nav-link active" data-i18n="nav.secrets">Secrets</a>
|
||||
<a href="/logs" class="mobile-nav-link" data-i18n="nav.logs">Logs</a>
|
||||
<a href="/usage" class="mobile-nav-link" data-i18n="nav.usage">Usage</a>
|
||||
</div>
|
||||
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div class="page-header-text">
|
||||
<h1 class="page-title" data-i18n="secrets.title">Secrets</h1>
|
||||
<p class="page-subtitle" data-i18n="secrets.subtitle">Encrypted upstream credentials for bearer tokens, basic auth, and API keys used by auth profiles.</p>
|
||||
</div>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-primary" id="btn-create-secret" data-testid="secret-create-button" type="button">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="currentColor"><path d="M7.75 2a.75.75 0 01.75.75V7h4.25a.75.75 0 010 1.5H8.5v4.25a.75.75 0 01-1.5 0V8.5H2.75a.75.75 0 010-1.5H7V2.75A.75.75 0 017.75 2z"/></svg>
|
||||
<span data-i18n="secrets.new">Create secret</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="callout info">
|
||||
<svg class="callout-icon" width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="var(--blue)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="8" cy="8" r="6.5"/>
|
||||
<path d="M8 11V8M8 5.5V5"/>
|
||||
</svg>
|
||||
<div>
|
||||
<strong data-i18n="secrets.callout.title">Secret plaintext is write-only.</strong>
|
||||
<span data-i18n="secrets.callout.body">Crank encrypts secret values with the workspace master key. After create or rotate, the API returns only metadata, so this page focuses on lifecycle and usage references.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-card" style="margin-top: 20px;">
|
||||
<div class="section-card-header">
|
||||
<div>
|
||||
<div class="section-card-title" data-i18n="secrets.active.title">Workspace secrets</div>
|
||||
<div class="section-card-subtitle" id="secrets-summary"></div>
|
||||
</div>
|
||||
<div class="filter-bar-search" style="max-width: 240px; margin: 0;">
|
||||
<svg width="13" height="13" style="position:absolute; left:9px; top:50%; transform:translateY(-50%); color:var(--text-muted);" viewBox="0 0 16 16" fill="currentColor"><path d="M10.68 11.74a6 6 0 01-7.922-8.982 6 6 0 018.982 7.922l3.04 3.04a.749.749 0 01-.326 1.275.749.749 0 01-.734-.215zM11.5 7a4.499 4.499 0 11-8.997 0A4.499 4.499 0 0111.5 7z"/></svg>
|
||||
<input type="text" id="secret-search" data-i18n-ph="secrets.search" placeholder="Filter secrets…" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-card-body" style="padding-top: 0;">
|
||||
<div id="secrets-table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="secrets.th.name">Name</th>
|
||||
<th data-i18n="secrets.th.kind">Kind</th>
|
||||
<th data-i18n="secrets.th.version">Version</th>
|
||||
<th data-i18n="secrets.th.last_used">Last used</th>
|
||||
<th data-i18n="secrets.th.used_by">Used by</th>
|
||||
<th data-i18n="secrets.th.status">Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="secrets-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="resource-list secrets-card-list" id="secrets-card-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-card" style="margin-top: 20px;">
|
||||
<div class="section-card-header">
|
||||
<div>
|
||||
<div class="section-card-title" data-i18n="secrets.profiles.title">Auth profile references</div>
|
||||
<div class="section-card-subtitle" id="secret-profiles-summary" data-i18n="secrets.profiles.subtitle">Profiles resolve secrets at runtime before upstream execution.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-card-body">
|
||||
<div class="resource-list" id="auth-profiles-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-overlay" id="secret-modal" data-testid="secret-modal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title" id="secret-modal-title" data-i18n="secrets.modal.create_title">Create secret</span>
|
||||
<button class="modal-close" id="secret-modal-close-btn" type="button">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
|
||||
<line x1="1" y1="1" x2="11" y2="11"/><line x1="11" y1="1" x2="1" y2="11"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="secrets.modal.name">Secret name</label>
|
||||
<input class="field-input" id="secret-name" data-testid="secret-name-input" type="text" autocomplete="off">
|
||||
<div class="field-hint" data-i18n="secrets.modal.name_hint">Use a stable operator-facing label. Plaintext values are never returned after storage.</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="secrets.modal.kind">Secret kind</label>
|
||||
<select class="field-select" id="secret-kind" data-testid="secret-kind-select">
|
||||
<option value="token" data-i18n="secrets.kind.token">Token</option>
|
||||
<option value="username_password" data-i18n="secrets.kind.username_password">Username/password</option>
|
||||
<option value="header" data-i18n="secrets.kind.header">Header value</option>
|
||||
<option value="generic" data-i18n="secrets.kind.generic">Generic JSON</option>
|
||||
</select>
|
||||
<div class="field-hint" id="secret-kind-hint"></div>
|
||||
</div>
|
||||
|
||||
<div class="secret-value-grid" id="secret-value-fields">
|
||||
<div class="field-group" data-kind-field="string">
|
||||
<label class="field-label" id="secret-string-label" data-i18n="secrets.modal.value">Secret value</label>
|
||||
<input class="field-input" id="secret-string-value" data-testid="secret-value-input" type="text" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="field-row" data-kind-field="basic" hidden>
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="secrets.modal.username">Username</label>
|
||||
<input class="field-input" id="secret-username" data-testid="secret-username-input" type="text" autocomplete="off">
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="secrets.modal.password">Password</label>
|
||||
<input class="field-input" id="secret-password" data-testid="secret-password-input" type="password" autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group" data-kind-field="json" hidden>
|
||||
<label class="field-label" data-i18n="secrets.modal.json_payload">JSON payload</label>
|
||||
<textarea class="field-textarea code-textarea" id="secret-json-value" data-testid="secret-json-input" rows="8" spellcheck="false"></textarea>
|
||||
<div class="field-hint" data-i18n="secrets.modal.json_hint">Useful for generic secret payloads. The value must be valid JSON.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="secret-inline-note" id="secret-modal-note" data-i18n="secrets.modal.create_note">Creation stores encrypted plaintext and returns metadata only.</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" id="secret-modal-cancel-btn" type="button" data-i18n="secrets.modal.cancel">Cancel</button>
|
||||
<button class="btn-primary" id="secret-modal-submit-btn" data-testid="secret-submit-button" type="button" data-i18n="secrets.modal.create_action">Create secret</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="%CRANK_BUNDLE_SECRETS%"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,330 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<base href="../">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Crank — Settings</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/variables.css">
|
||||
<link rel="stylesheet" href="css/layout.css">
|
||||
<link rel="stylesheet" href="css/pages.css">
|
||||
<link rel="stylesheet" href="css/settings.css">
|
||||
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar">
|
||||
<a href="/" class="nav-logo">
|
||||
<div class="nav-logo-mark"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="white"><path fill-rule="evenodd" d="M 10.000,1.800 L 12.287,3.962 L 15.408,3.557 L 15.987,6.650 L 18.750,8.157 L 17.400,11.000 L 18.750,13.843 L 15.987,15.350 L 15.408,18.443 L 12.287,18.038 L 10.000,20.200 L 7.713,18.038 L 4.592,18.443 L 4.013,15.350 L 1.250,13.843 L 2.600,11.000 L 1.250,8.157 L 4.013,6.650 L 4.592,3.557 L 7.713,3.962 Z M 12.800,11.000 A 2.8 2.8 0 1 0 7.200,11.000 A 2.8 2.8 0 1 0 12.800,11.000 Z"/><path fill-rule="evenodd" d="M 11.791,15.604 Q 16.083,17.179 21.375,21.154 A 2.00 2.00 0 1 1 23.625,17.846 Q 19.782,15.610 14.940,10.973 Z M 24.300,19.500 A 1.8 1.8 0 1 0 20.700,19.500 A 1.8 1.8 0 1 0 24.300,19.500 Z"/><path d="M 10.0,9.7 L 11.3,11.0 L 10.0,12.3 L 8.7,11.0 Z" fill="rgba(0,0,0,0.3)"/></svg></div>
|
||||
<span class="nav-logo-text">Crank</span>
|
||||
</a>
|
||||
|
||||
<!-- Workspace switcher -->
|
||||
<div class="ws-switcher" id="ws-switcher">
|
||||
<button class="ws-switcher-trigger" onclick="toggleWsSwitcher(event)">
|
||||
<div class="ws-dot" id="ws-dot">A</div>
|
||||
<span class="ws-current-name" id="ws-current-name">acme-workspace</span>
|
||||
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<div class="ws-dropdown" id="ws-dropdown" hidden>
|
||||
<div class="ws-dropdown-label" data-i18n="nav.workspaces">Your workspaces</div>
|
||||
<div id="ws-dropdown-list"></div>
|
||||
<div class="ws-dropdown-footer">
|
||||
<button class="ws-dropdown-create" onclick="window.location.href='/workspace-setup?mode=create'">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
|
||||
<span data-i18n="nav.create_workspace">Create workspace</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-links">
|
||||
<a href="/" class="nav-link" data-i18n="nav.operations">Operations</a>
|
||||
<a href="/agents" class="nav-link" data-i18n="nav.agents">Agents</a>
|
||||
<a href="/api-keys" class="nav-link" data-i18n="nav.apikeys">API Keys</a>
|
||||
<a href="/secrets" class="nav-link" data-i18n="nav.secrets">Secrets</a>
|
||||
<a href="/logs" class="nav-link" data-i18n="nav.logs">Logs</a>
|
||||
<a href="/usage" class="nav-link" data-i18n="nav.usage">Usage</a>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<button class="nav-hamburger" data-i18n-title="nav.menu" data-i18n-aria-label="nav.menu" aria-label="Menu"><span></span><span></span><span></span></button>
|
||||
<button class="nav-icon-btn" data-i18n-title="nav.notifications" title="Notifications">
|
||||
<svg width="15" height="15"><use href="icons/general/bell.svg#icon"/></svg>
|
||||
</button>
|
||||
<div class="nav-divider"></div>
|
||||
<div class="user-menu">
|
||||
<div class="nav-avatar" data-i18n-title="nav.account" title="Account">AT</div>
|
||||
<div class="user-dropdown">
|
||||
<div class="user-dropdown-header">
|
||||
<div class="user-dropdown-name">Crank</div>
|
||||
<div class="user-dropdown-role">—</div>
|
||||
</div>
|
||||
<button class="user-dropdown-item" data-action="settings-link">
|
||||
<svg width="13" height="13"><use href="icons/general/settings.svg#icon"/></svg>
|
||||
<span data-i18n="nav.settings">Settings</span>
|
||||
</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="user-dropdown-item danger" data-action="logout">
|
||||
<svg width="13" height="13"><use href="icons/general/logout.svg#icon"/></svg>
|
||||
<span data-i18n="nav.logout">Log out</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="mobile-nav" hidden>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.operations">Operations</a>
|
||||
<a href="/agents" class="mobile-nav-link" data-i18n="nav.agents">Agents</a>
|
||||
<a href="/api-keys" class="mobile-nav-link" data-i18n="nav.apikeys">API Keys</a>
|
||||
<a href="/secrets" class="mobile-nav-link" data-i18n="nav.secrets">Secrets</a>
|
||||
<a href="/logs" class="mobile-nav-link" data-i18n="nav.logs">Logs</a>
|
||||
<a href="/usage" class="mobile-nav-link" data-i18n="nav.usage">Usage</a>
|
||||
</div>
|
||||
|
||||
<div class="page">
|
||||
|
||||
<div class="page-header" style="margin-bottom: 24px;">
|
||||
<div class="page-header-text">
|
||||
<h1 class="page-title" data-i18n="settings.page.title">Account settings</h1>
|
||||
<p class="page-subtitle" data-i18n="settings.page.subtitle">Manage the live profile, password and workspace settings available in this build. Planned capabilities stay visible, but they do not pretend to be wired yet.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-layout">
|
||||
|
||||
<!-- ── Sidebar nav ── -->
|
||||
<nav class="settings-nav" id="settings-nav">
|
||||
<button class="settings-nav-item" data-section="workspace">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2 5.5h12M4 2.5h8a1.5 1.5 0 011.5 1.5v8A1.5 1.5 0 0112 13.5H4A1.5 1.5 0 012.5 12V4A1.5 1.5 0 014 2.5z"/>
|
||||
</svg>
|
||||
<span data-i18n="settings.nav.workspace">Workspace</span>
|
||||
</button>
|
||||
<button class="settings-nav-item active" data-section="profile">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="8" cy="5" r="3"/>
|
||||
<path d="M1.5 14.5c0-3.314 2.91-6 6.5-6s6.5 2.686 6.5 6"/>
|
||||
</svg>
|
||||
<span data-i18n="settings.nav.profile">Profile</span>
|
||||
</button>
|
||||
<button class="settings-nav-item" data-section="security">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M8 1L2 3.5v4.5C2 11.3 4.7 14.4 8 15c3.3-.6 6-3.7 6-7V3.5L8 1z"/>
|
||||
</svg>
|
||||
<span data-i18n="settings.nav.security">Security</span>
|
||||
</button>
|
||||
<button class="settings-nav-item" data-section="notifications">
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M8 1.5a5 5 0 015 5v2.5l1 2H2l1-2V6.5a5 5 0 015-5z"/>
|
||||
<path d="M6.5 13a1.5 1.5 0 003 0"/>
|
||||
</svg>
|
||||
<span data-i18n="settings.nav.notif">Notifications</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- ── Content ── -->
|
||||
<div id="settings-content">
|
||||
|
||||
<div id="section-workspace" class="section-anchor">
|
||||
<div class="section-card" style="margin-bottom: 20px;">
|
||||
<div class="section-card-header">
|
||||
<div>
|
||||
<div class="section-card-title" data-i18n="settings.ws.title">Workspace settings</div>
|
||||
<div class="section-card-subtitle" data-i18n="settings.ws.subtitle">General information about your workspace</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-card-body" style="padding-bottom:8px;">
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="settings.ws.name">Workspace name</label>
|
||||
<input class="field-input" id="settings-ws-slug" type="text" value="acme-workspace" autocomplete="off">
|
||||
<div class="field-hint" data-i18n="settings.ws.slug_hint">Used in API endpoints and across the console. Only lowercase letters, numbers and hyphens.</div>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="settings.ws.display">Display name</label>
|
||||
<input class="field-input" id="settings-ws-display-name" type="text" value="Acme Corp" autocomplete="off">
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label"><span data-i18n="settings.ws.description_optional">Description (optional)</span></label>
|
||||
<textarea class="field-textarea" id="settings-ws-description" rows="2" data-i18n-ph="settings.ws.description_placeholder" placeholder="What does this workspace do?">Primary production workspace for CRM and e-commerce API operations.</textarea>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="settings.ws.tz">Timezone</label>
|
||||
<select class="field-select" id="settings-ws-timezone">
|
||||
<option selected>UTC</option>
|
||||
<option>America/New_York</option>
|
||||
<option>America/Los_Angeles</option>
|
||||
<option>Europe/London</option>
|
||||
<option>Europe/Paris</option>
|
||||
<option>Asia/Tokyo</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="settings.ws.protocol">Default protocol</label>
|
||||
<select class="field-select" id="settings-ws-protocol">
|
||||
<option value="rest" selected>REST / HTTP</option>
|
||||
</select>
|
||||
<div class="field-hint" id="settings-ws-protocol-hint" data-i18n="settings.ws.protocol_hint">This build limits the default protocol to the capabilities available in the current edition.</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Language -->
|
||||
<div class="field-group" style="margin-top:4px;">
|
||||
<label class="field-label">
|
||||
<span data-i18n="settings.lang.title">Language</span>
|
||||
</label>
|
||||
<div class="lang-switcher">
|
||||
<button class="lang-btn active" data-lang="en" onclick="setLang('en')">
|
||||
<span class="lang-flag">🇬🇧</span>
|
||||
<span data-i18n="settings.lang.en">English</span>
|
||||
</button>
|
||||
<button class="lang-btn" data-lang="ru" onclick="setLang('ru')">
|
||||
<span class="lang-flag">🇷🇺</span>
|
||||
<span data-i18n="settings.lang.ru">Русский</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="field-hint" data-i18n="settings.lang.subtitle">Interface display language</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;justify-content:flex-end;padding-top:4px;">
|
||||
<button class="btn-primary" id="settings-ws-save-btn" type="button"><span data-i18n="btn.save">Save changes</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ■ PROFILE ■ -->
|
||||
<div id="section-profile" class="section-anchor">
|
||||
<div class="section-card" style="margin-bottom: 20px;">
|
||||
<div class="section-card-header">
|
||||
<div class="section-card-title" data-i18n="settings.profile.title">Profile</div>
|
||||
</div>
|
||||
<div class="section-card-body">
|
||||
<div class="avatar-upload">
|
||||
<div class="avatar-large" id="profile-avatar">AT</div>
|
||||
<div class="avatar-upload-actions">
|
||||
<div class="avatar-name" id="profile-display-name">Crank</div>
|
||||
<div class="avatar-sub" id="profile-email">operator@acme-workspace</div>
|
||||
<div class="field-hint" data-i18n="settings.profile.avatar_hint">Your avatar is generated from your display name and email.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-hint" style="margin-bottom:16px;" data-i18n="settings.profile.backend_hint">Your name and email are stored on the backend and used across the console and session identity.</div>
|
||||
|
||||
<div class="field-row">
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="settings.profile.first_name">First name</label>
|
||||
<input class="field-input" id="field-firstname" type="text" value="Crank" autocomplete="given-name">
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="settings.profile.last_name">Last name</label>
|
||||
<input class="field-input" id="field-lastname" type="text" value="" autocomplete="family-name" data-i18n-ph="settings.profile.last_name_placeholder" placeholder="Optional">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="settings.profile.email_label">Email address</label>
|
||||
<input class="field-input" id="field-email" type="email" value="operator@acme.com" autocomplete="email">
|
||||
<div class="field-hint" data-i18n="settings.profile.email_hint">Changing email updates the login identity for the current account.</div>
|
||||
</div>
|
||||
|
||||
<div class="field-hint" id="settings-profile-status" style="margin-bottom:12px;"></div>
|
||||
<div style="display:flex;justify-content:flex-end;">
|
||||
<button class="btn-primary" id="settings-profile-save-btn" type="button" data-i18n="settings.profile.save">Save profile</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ■ SECURITY ■ -->
|
||||
<div id="section-security" class="section-anchor">
|
||||
<div class="section-card" style="margin-bottom: 20px;">
|
||||
<div class="section-card-header">
|
||||
<div class="section-card-title" data-i18n="settings.security.title">Security</div>
|
||||
</div>
|
||||
<div class="section-card-body" style="padding-bottom:8px;">
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="settings.security.current_password">Current password</label>
|
||||
<input class="field-input" id="security-current-password" type="password" placeholder="••••••••" autocomplete="current-password">
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="settings.security.new_password">New password</label>
|
||||
<input class="field-input" id="security-new-password" type="password" data-i18n-ph="settings.security.new_password_placeholder" placeholder="min. 12 characters" autocomplete="new-password">
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="settings.security.confirm_password">Confirm new password</label>
|
||||
<input class="field-input" id="security-confirm-password" type="password" placeholder="••••••••" autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-hint" id="settings-password-status" style="margin-bottom:12px;"></div>
|
||||
<div style="display:flex;justify-content:flex-end;padding-bottom:8px;">
|
||||
<button class="btn-primary" id="settings-password-save-btn" type="button" data-i18n="settings.security.change_password">Change password</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 4px 20px 16px;">
|
||||
<div class="settings-section-divider" style="margin-top:0;"></div>
|
||||
<div class="planned-section-title">
|
||||
<span data-i18n="settings.security.capabilities_title">More security options</span>
|
||||
</div>
|
||||
<div class="callout warning" style="margin-bottom:0;">
|
||||
<svg class="callout-icon" width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="var(--amber)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="8,1.5 15.5,14.5 0.5,14.5" fill="none"/><path d="M8 6v4M8 11.5v.5"/>
|
||||
</svg>
|
||||
<div>
|
||||
<strong id="settings-security-capability-title" data-i18n="settings.security.not_available">Available later.</strong> <span id="settings-security-capability-body" data-i18n="settings.security.capabilities_body">Two-factor authentication, passkeys, and multi-session controls will be added later. The current live flow uses password sign-in with one HttpOnly browser session.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-slot="settings.sso_panel"></div>
|
||||
<div data-slot="settings.totp_panel"></div>
|
||||
<div data-slot="settings.audit_panel"></div>
|
||||
<div data-slot="settings.billing_panel"></div>
|
||||
</div>
|
||||
|
||||
<div class="section-card" style="margin-bottom: 20px;">
|
||||
<div class="section-card-header">
|
||||
<div class="section-card-title" data-i18n="settings.session.title">Current session</div>
|
||||
</div>
|
||||
<div style="padding: 8px 20px 16px;">
|
||||
<div class="member-row">
|
||||
<div class="member-info">
|
||||
<div class="member-name"><span data-i18n="settings.session.browser">Browser session</span> — <span style="color:var(--green);font-size:12px;" data-i18n="settings.session.current">current</span></div>
|
||||
<div class="member-email" id="settings-session-summary" data-i18n="settings.session.loading">Loading current session details…</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ■ NOTIFICATIONS ■ -->
|
||||
<div id="section-notifications" class="section-anchor">
|
||||
<div class="section-card" style="margin-bottom: 20px;">
|
||||
<div class="section-card-header">
|
||||
<div class="section-card-title" data-i18n="settings.notifications.title">Notifications</div>
|
||||
</div>
|
||||
<div style="padding: 8px 20px 16px;">
|
||||
<div class="callout info" style="margin-bottom:14px;">
|
||||
<svg class="callout-icon" width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="var(--blue)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="8" cy="8" r="6.5"/>
|
||||
<path d="M8 11V8M8 5.5V5"/>
|
||||
</svg>
|
||||
<div>
|
||||
<strong id="settings-notifications-capability-title" data-i18n="settings.notifications.not_ready_title">Not included in this build.</strong> <span id="settings-notifications-capability-body" data-i18n="settings.notifications.not_ready_body">Notification routing and personal preferences are outside the current Community surface.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /settings-content -->
|
||||
</div><!-- /settings-layout -->
|
||||
|
||||
</div><!-- /page -->
|
||||
|
||||
<script src="%CRANK_BUNDLE_SETTINGS%"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,234 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<base href="../">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Crank — Usage</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/variables.css">
|
||||
<link rel="stylesheet" href="css/layout.css">
|
||||
<link rel="stylesheet" href="css/pages.css">
|
||||
<link rel="stylesheet" href="css/usage.css">
|
||||
<style>
|
||||
.usage-card-list { display: none; }
|
||||
@media (max-width: 720px) {
|
||||
#usage-table-wrap { display: none; }
|
||||
.usage-card-list { display: grid; }
|
||||
}
|
||||
</style>
|
||||
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="navbar">
|
||||
<a href="/" class="nav-logo">
|
||||
<div class="nav-logo-mark"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="white"><path fill-rule="evenodd" d="M 10.000,1.800 L 12.287,3.962 L 15.408,3.557 L 15.987,6.650 L 18.750,8.157 L 17.400,11.000 L 18.750,13.843 L 15.987,15.350 L 15.408,18.443 L 12.287,18.038 L 10.000,20.200 L 7.713,18.038 L 4.592,18.443 L 4.013,15.350 L 1.250,13.843 L 2.600,11.000 L 1.250,8.157 L 4.013,6.650 L 4.592,3.557 L 7.713,3.962 Z M 12.800,11.000 A 2.8 2.8 0 1 0 7.200,11.000 A 2.8 2.8 0 1 0 12.800,11.000 Z"/><path fill-rule="evenodd" d="M 11.791,15.604 Q 16.083,17.179 21.375,21.154 A 2.00 2.00 0 1 1 23.625,17.846 Q 19.782,15.610 14.940,10.973 Z M 24.300,19.500 A 1.8 1.8 0 1 0 20.700,19.500 A 1.8 1.8 0 1 0 24.300,19.500 Z"/><path d="M 10.0,9.7 L 11.3,11.0 L 10.0,12.3 L 8.7,11.0 Z" fill="rgba(0,0,0,0.3)"/></svg></div>
|
||||
<span class="nav-logo-text">Crank</span>
|
||||
</a>
|
||||
|
||||
<!-- Workspace switcher -->
|
||||
<div class="ws-switcher" id="ws-switcher">
|
||||
<button class="ws-switcher-trigger" onclick="toggleWsSwitcher(event)">
|
||||
<div class="ws-dot" id="ws-dot">A</div>
|
||||
<span class="ws-current-name" id="ws-current-name">acme-workspace</span>
|
||||
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<div class="ws-dropdown" id="ws-dropdown" hidden>
|
||||
<div class="ws-dropdown-label" data-i18n="nav.workspaces">Your workspaces</div>
|
||||
<div id="ws-dropdown-list"></div>
|
||||
<div class="ws-dropdown-footer">
|
||||
<button class="ws-dropdown-create" onclick="window.location.href='/workspace-setup?mode=create'">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
|
||||
<span data-i18n="nav.create_workspace">Create workspace</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-links">
|
||||
<a href="/" class="nav-link" data-i18n="nav.operations">Operations</a>
|
||||
<a href="/agents" class="nav-link" data-i18n="nav.agents">Agents</a>
|
||||
<a href="/api-keys" class="nav-link" data-i18n="nav.apikeys">API Keys</a>
|
||||
<a href="/secrets" class="nav-link" data-i18n="nav.secrets">Secrets</a>
|
||||
<a href="/logs" class="nav-link" data-i18n="nav.logs">Logs</a>
|
||||
<a href="/usage" class="nav-link active" data-i18n="nav.usage">Usage</a>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
<button class="nav-hamburger" data-i18n-title="nav.menu" data-i18n-aria-label="nav.menu" aria-label="Menu"><span></span><span></span><span></span></button>
|
||||
<button class="nav-icon-btn" data-i18n-title="nav.notifications" title="Notifications">
|
||||
<svg width="15" height="15"><use href="icons/general/bell.svg#icon"/></svg>
|
||||
</button>
|
||||
<div class="nav-divider"></div>
|
||||
<div class="user-menu">
|
||||
<div class="nav-avatar" data-i18n-title="nav.account" title="Account">AT</div>
|
||||
<div class="user-dropdown">
|
||||
<div class="user-dropdown-header">
|
||||
<div class="user-dropdown-name">Crank</div>
|
||||
<div class="user-dropdown-role">—</div>
|
||||
</div>
|
||||
<button class="user-dropdown-item" data-action="settings-link">
|
||||
<svg width="13" height="13"><use href="icons/general/settings.svg#icon"/></svg>
|
||||
<span data-i18n="nav.settings">Settings</span>
|
||||
</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="user-dropdown-item danger" data-action="logout">
|
||||
<svg width="13" height="13"><use href="icons/general/logout.svg#icon"/></svg>
|
||||
<span data-i18n="nav.logout">Log out</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="mobile-nav" hidden>
|
||||
<a href="/" class="mobile-nav-link" data-i18n="nav.operations">Operations</a>
|
||||
<a href="/agents" class="mobile-nav-link" data-i18n="nav.agents">Agents</a>
|
||||
<a href="/api-keys" class="mobile-nav-link" data-i18n="nav.apikeys">API Keys</a>
|
||||
<a href="/secrets" class="mobile-nav-link" data-i18n="nav.secrets">Secrets</a>
|
||||
<a href="/logs" class="mobile-nav-link" data-i18n="nav.logs">Logs</a>
|
||||
<a href="/usage" class="mobile-nav-link active" data-i18n="nav.usage">Usage</a>
|
||||
</div>
|
||||
|
||||
<div class="page">
|
||||
|
||||
<div class="page-header">
|
||||
<div class="page-header-text">
|
||||
<h1 class="page-title" data-i18n="usage.title">Usage</h1>
|
||||
<p class="page-subtitle" data-i18n="usage.subtitle">Invocation metrics for all operations in your workspace.</p>
|
||||
</div>
|
||||
<div class="page-header-actions">
|
||||
<select class="period-select" id="period">
|
||||
<option value="7d" selected data-i18n="usage.period.7d">Last 7 days</option>
|
||||
<option value="30d" data-i18n="usage.period.30d">Last 30 days</option>
|
||||
<option value="90d" data-i18n="usage.period.90d">Last 90 days</option>
|
||||
<option value="this_month" data-i18n="usage.period.this_month">This month</option>
|
||||
</select>
|
||||
<button class="btn-secondary" type="button">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="currentColor"><path d="M7.47 1.72a.75.75 0 011.06 0l4.5 4.5a.75.75 0 01-1.06 1.06L8.75 4.06v8.94a.75.75 0 01-1.5 0V4.06L4.53 7.28a.75.75 0 01-1.06-1.06l4.5-4.5zM1.5 13.75a.75.75 0 01.75-.75h11.5a.75.75 0 010 1.5H2.25a.75.75 0 01-.75-.75z"/></svg>
|
||||
<span data-i18n="usage.export">Export CSV</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stat cards -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-i18n="usage.stats.total">Total invocations</div>
|
||||
<div class="stat-value">28,341</div>
|
||||
<div class="stat-delta up">
|
||||
<svg width="11" height="11" viewBox="0 0 12 12" fill="currentColor"><path d="M6 2l4 4H7v4H5V6H2l4-4z"/></svg>
|
||||
+12% vs prior period
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-i18n="usage.stats.success">Success rate</div>
|
||||
<div class="stat-value">98.6%</div>
|
||||
<div class="stat-delta up">
|
||||
<svg width="11" height="11" viewBox="0 0 12 12" fill="currentColor"><path d="M6 2l4 4H7v4H5V6H2l4-4z"/></svg>
|
||||
+0.3pp vs prior period
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-i18n="usage.stats.p50">Median latency (p50)</div>
|
||||
<div class="stat-value">124ms</div>
|
||||
<div class="stat-delta up">
|
||||
<svg width="11" height="11" viewBox="0 0 12 12" fill="currentColor"><path d="M6 2l4 4H7v4H5V6H2l4-4z"/></svg>
|
||||
-18ms vs prior period
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label" data-i18n="usage.stats.p99">p99 latency</div>
|
||||
<div class="stat-value">2.1s</div>
|
||||
<div class="stat-delta down">
|
||||
<svg width="11" height="11" viewBox="0 0 12 12" fill="currentColor"><path d="M6 10L2 6h3V2h2v4h3l-4 4z"/></svg>
|
||||
+340ms vs prior period
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invocations chart -->
|
||||
<div class="section-card" style="margin-bottom:20px;">
|
||||
<div class="section-card-header">
|
||||
<div class="section-card-title" data-i18n="usage.chart.title">Invocations over time</div>
|
||||
<div style="display:flex;align-items:center;gap:12px;font-size:12px;color:var(--text-muted);">
|
||||
<span style="display:flex;align-items:center;gap:5px;"><span style="width:10px;height:10px;border-radius:2px;background:var(--accent);display:inline-block;"></span><span data-i18n="usage.chart.success">Success</span></span>
|
||||
<span style="display:flex;align-items:center;gap:5px;"><span style="width:10px;height:10px;border-radius:2px;background:var(--red);display:inline-block;"></span><span data-i18n="usage.chart.error">Error</span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-card-body" style="padding-bottom:28px;">
|
||||
<div class="chart-placeholder" id="chart-bars"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Per-operation table -->
|
||||
<div class="section-card">
|
||||
<div class="section-card-header">
|
||||
<div>
|
||||
<div class="section-card-title" data-i18n="usage.table.title">By operation</div>
|
||||
<div class="section-card-subtitle">Breakdown for last 7 days</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="usage-table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="usage.table.th.operation">Operation</th>
|
||||
<th data-i18n="usage.table.th.protocol">Protocol</th>
|
||||
<th style="text-align:right;" data-i18n="usage.table.th.calls">Calls</th>
|
||||
<th style="text-align:right;" data-i18n="usage.table.th.errors">Errors</th>
|
||||
<th style="text-align:right;" data-i18n="usage.table.th.error_rate">Error rate</th>
|
||||
<th data-i18n="usage.table.th.latency">Latency (p50 / p95 / p99)</th>
|
||||
<th style="text-align:right;" data-i18n="usage.table.th.share">Traffic share</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="usage-tbody">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="resource-list usage-card-list" id="usage-card-list"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Template: chart bar column -->
|
||||
<template id="tmpl-chart-bar">
|
||||
<div class="chart-col">
|
||||
<div class="chart-col-bars">
|
||||
<div class="chart-bar error"></div>
|
||||
<div class="chart-bar success"></div>
|
||||
</div>
|
||||
<div class="chart-col-label"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Template: usage stats table row -->
|
||||
<template id="tmpl-usage-row">
|
||||
<tr>
|
||||
<td>
|
||||
<div class="col-name"></div>
|
||||
<div class="col-op-slug" style="font-family:monospace;font-size:11px;color:var(--text-muted);"></div>
|
||||
</td>
|
||||
<td><span class="badge col-proto" style="border-color:rgba(88,166,255,0.25);background:rgba(88,166,255,0.07);color:var(--blue);"></span></td>
|
||||
<td class="col-calls" style="text-align:right;font-variant-numeric:tabular-nums;"></td>
|
||||
<td class="col-errors" style="text-align:right;font-variant-numeric:tabular-nums;"></td>
|
||||
<td class="col-err-rate" style="text-align:right;font-variant-numeric:tabular-nums;"></td>
|
||||
<td class="col-latency">
|
||||
<div class="latency-bar">
|
||||
<div class="latency-seg latency-p50"></div>
|
||||
<div class="latency-seg latency-p95"></div>
|
||||
<div class="latency-seg latency-p99"></div>
|
||||
</div>
|
||||
<div class="col-latency-text" style="font-size:11px;color:var(--text-muted);margin-top:3px;font-family:monospace;"></div>
|
||||
</td>
|
||||
<td style="text-align:right;min-width:120px;">
|
||||
<div class="col-quota-label" style="font-size:11.5px;color:var(--text-muted);margin-bottom:3px;"></div>
|
||||
<div class="quota-bar-wrap"><div class="quota-bar-fill col-quota-fill"></div></div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script src="%CRANK_BUNDLE_USAGE%"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,267 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Crank — New Operation</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="../../css/variables.css">
|
||||
<link rel="stylesheet" href="../../css/layout.css">
|
||||
<link rel="stylesheet" href="../../css/wizard.css">
|
||||
<link rel="stylesheet" href="../../css/pages.css">
|
||||
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
|
||||
</head>
|
||||
<body class="wizard-page">
|
||||
|
||||
<!-- ═══════════════════ NAVBAR ═══════════════════ -->
|
||||
<nav class="navbar">
|
||||
<a href="/" class="nav-logo">
|
||||
<div class="nav-logo-mark"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" fill="white"><path fill-rule="evenodd" d="M 10.000,1.800 L 12.287,3.962 L 15.408,3.557 L 15.987,6.650 L 18.750,8.157 L 17.400,11.000 L 18.750,13.843 L 15.987,15.350 L 15.408,18.443 L 12.287,18.038 L 10.000,20.200 L 7.713,18.038 L 4.592,18.443 L 4.013,15.350 L 1.250,13.843 L 2.600,11.000 L 1.250,8.157 L 4.013,6.650 L 4.592,3.557 L 7.713,3.962 Z M 12.800,11.000 A 2.8 2.8 0 1 0 7.200,11.000 A 2.8 2.8 0 1 0 12.800,11.000 Z"/><path fill-rule="evenodd" d="M 11.791,15.604 Q 16.083,17.179 21.375,21.154 A 2.00 2.00 0 1 1 23.625,17.846 Q 19.782,15.610 14.940,10.973 Z M 24.300,19.500 A 1.8 1.8 0 1 0 20.700,19.500 A 1.8 1.8 0 1 0 24.300,19.500 Z"/><path d="M 10.0,9.7 L 11.3,11.0 L 10.0,12.3 L 8.7,11.0 Z" fill="rgba(0,0,0,0.3)"/></svg></div>
|
||||
<span class="nav-logo-text">Crank</span>
|
||||
</a>
|
||||
|
||||
<!-- Workspace switcher -->
|
||||
<div class="ws-switcher" id="ws-switcher">
|
||||
<button class="ws-switcher-trigger" onclick="toggleWsSwitcher(event)">
|
||||
<div class="ws-dot" id="ws-dot">A</div>
|
||||
<span class="ws-current-name" id="ws-current-name" data-testid="shell-workspace-name">acme-workspace</span>
|
||||
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6l4 4 4-4"/></svg>
|
||||
</button>
|
||||
<div class="ws-dropdown" id="ws-dropdown" hidden>
|
||||
<div class="ws-dropdown-label" data-i18n="nav.workspaces">Your workspaces</div>
|
||||
<div id="ws-dropdown-list"></div>
|
||||
<div class="ws-dropdown-footer">
|
||||
<button class="ws-dropdown-create" onclick="window.location.href='/workspace-setup?mode=create'">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M8 3v10M3 8h10"/></svg>
|
||||
<span data-i18n="nav.create_workspace">Create workspace</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-links">
|
||||
<a href="/" class="nav-link active" data-i18n="nav.operations">Operations</a>
|
||||
<a href="/agents" class="nav-link" data-i18n="nav.agents">Agents</a>
|
||||
<a href="/api-keys" class="nav-link" data-i18n="nav.apikeys">API Keys</a>
|
||||
<a href="/secrets" class="nav-link" data-i18n="nav.secrets">Secrets</a>
|
||||
<a href="/logs" class="nav-link" data-i18n="nav.logs">Logs</a>
|
||||
<a href="/usage" class="nav-link" data-i18n="nav.usage">Usage</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-right">
|
||||
<button class="nav-hamburger" data-i18n-title="nav.menu" data-i18n-aria-label="nav.menu" aria-label="Menu"><span></span><span></span><span></span></button>
|
||||
|
||||
|
||||
<button class="nav-icon-btn" data-i18n-title="nav.notifications" title="Notifications">
|
||||
<svg width="15" height="15"><use href="../../icons/general/bell.svg#icon"/></svg>
|
||||
</button>
|
||||
|
||||
<div class="nav-divider"></div>
|
||||
|
||||
<div class="user-menu">
|
||||
<div class="nav-avatar" data-testid="shell-avatar" data-i18n-title="nav.account" title="Account">AT</div>
|
||||
<div class="user-dropdown">
|
||||
<div class="user-dropdown-header">
|
||||
<div class="user-dropdown-name" data-testid="shell-user-name">Crank</div>
|
||||
<div class="user-dropdown-role" data-testid="shell-user-role">—</div>
|
||||
</div>
|
||||
<button class="user-dropdown-item" data-action="settings-link">
|
||||
<svg width="13" height="13"><use href="../../icons/general/settings.svg#icon"/></svg>
|
||||
<span data-i18n="nav.settings">Settings</span>
|
||||
</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="user-dropdown-item danger" data-action="logout">
|
||||
<svg width="13" height="13"><use href="../../icons/general/logout.svg#icon"/></svg>
|
||||
<span data-i18n="nav.logout">Log out</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="mobile-nav" hidden>
|
||||
<a href="/" class="mobile-nav-link active" data-i18n="nav.operations">Operations</a>
|
||||
<a href="/agents" class="mobile-nav-link" data-i18n="nav.agents">Agents</a>
|
||||
<a href="/api-keys" class="mobile-nav-link" data-i18n="nav.apikeys">API Keys</a>
|
||||
<a href="/secrets" class="mobile-nav-link" data-i18n="nav.secrets">Secrets</a>
|
||||
<a href="/logs" class="mobile-nav-link" data-i18n="nav.logs">Logs</a>
|
||||
<a href="/usage" class="mobile-nav-link" data-i18n="nav.usage">Usage</a>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════ WIZARD SHELL ═══════════════════ -->
|
||||
<div class="wizard-shell">
|
||||
|
||||
<!-- ══ TOP PROGRESS STRIP ══ -->
|
||||
<div class="progress-strip">
|
||||
<button class="back-to-catalog-btn" id="back-to-catalog" data-i18n-title="wizard.back_catalog" title="Back to catalog">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="currentColor"><path d="M9.78 12.78a.75.75 0 01-1.06 0L4.47 8.53a.75.75 0 010-1.06l4.25-4.25a.75.75 0 011.06 1.06L6.06 8l3.72 3.72a.75.75 0 010 1.06z"/></svg>
|
||||
<span data-i18n="wizard.back_catalog">Operations</span>
|
||||
</button>
|
||||
<div class="progress-divider"></div>
|
||||
<span class="progress-label" data-i18n="wizard.progress.create">Create operation</span>
|
||||
<div class="progress-bar-wrap">
|
||||
<div class="progress-bar-fill"></div>
|
||||
</div>
|
||||
<span class="progress-pct">20%</span>
|
||||
<button class="progress-close" data-i18n-title="wizard.exit" title="Exit wizard" aria-label="Exit wizard">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
|
||||
<line x1="1" y1="1" x2="11" y2="11"/><line x1="11" y1="1" x2="1" y2="11"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- ══ WIZARD BODY ══ -->
|
||||
<div class="wizard-body">
|
||||
|
||||
<!-- ── Step sidebar ── -->
|
||||
<div class="step-sidebar">
|
||||
<div class="step-sidebar-card">
|
||||
|
||||
<div class="step-sidebar-header">
|
||||
<div class="step-sidebar-logo-mark">M</div>
|
||||
<div class="step-sidebar-brand" data-wizard-brand="create">
|
||||
Create <span>operation</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-sidebar-body">
|
||||
<div class="step-sidebar-section-label" data-i18n="wizard.progress.section">Progress</div>
|
||||
|
||||
<div class="steps-list">
|
||||
|
||||
<div class="step-item active">
|
||||
<div class="step-indicator">1</div>
|
||||
<div class="step-content">
|
||||
<div class="step-number" data-step="1">Step 1</div>
|
||||
<div class="step-name" data-i18n="wizard.step_name.protocol">Protocol</div>
|
||||
<div class="step-status-text" data-i18n="wizard.status.in_progress">In progress</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-item pending">
|
||||
<div class="step-indicator">2</div>
|
||||
<div class="step-content">
|
||||
<div class="step-number" data-step="2">Step 2</div>
|
||||
<div class="step-name" data-i18n="wizard.step_name.upstream">Upstream target</div>
|
||||
<div class="step-status-text" data-i18n="wizard.status.not_started">Not started</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-item pending" id="sidebar-step-3">
|
||||
<div class="step-indicator">3</div>
|
||||
<div class="step-content">
|
||||
<div class="step-number" data-step="3">Step 3</div>
|
||||
<div class="step-name" id="sidebar-step-3-name">Request config</div>
|
||||
<div class="step-status-text" data-i18n="wizard.status.not_started">Not started</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-item pending">
|
||||
<div class="step-indicator">4</div>
|
||||
<div class="step-content">
|
||||
<div class="step-number" data-step="4">Step 4</div>
|
||||
<div class="step-name" data-i18n="wizard.step_name.tool">Tool config</div>
|
||||
<div class="step-status-text" data-i18n="wizard.status.not_started">Not started</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-item pending">
|
||||
<div class="step-indicator">5</div>
|
||||
<div class="step-content">
|
||||
<div class="step-number" data-step="5">Step 5</div>
|
||||
<div class="step-name" data-i18n="wizard.step_name.mapping">Mapping</div>
|
||||
<div class="step-status-text" data-i18n="wizard.status.not_started">Not started</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /steps-list -->
|
||||
|
||||
<div class="sidebar-help" style="margin-top: 20px;">
|
||||
<div class="sidebar-help-title">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="8" cy="8" r="7"/>
|
||||
<path d="M8 12v-4M8 5v-.5"/>
|
||||
</svg>
|
||||
<span data-i18n="wizard.help.title">Need help?</span>
|
||||
</div>
|
||||
<div class="sidebar-help-text">
|
||||
<span data-i18n="wizard.help.body">Unsure which protocol to choose? Read the</span>
|
||||
<a href="#" class="sidebar-help-link" data-i18n="wizard.help.link">protocol comparison guide →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /step-sidebar-body -->
|
||||
</div><!-- /step-sidebar-card -->
|
||||
</div><!-- /step-sidebar -->
|
||||
|
||||
<!-- ── Main step panel ── -->
|
||||
<div class="wizard-main"><div id="step-panel-container"><!-- steps loaded dynamically --></div></div>
|
||||
</div><!-- /wizard-body -->
|
||||
|
||||
<!-- ══ BOTTOM ACTION BAR ══ -->
|
||||
<div class="action-bar" role="toolbar" aria-label="Wizard navigation">
|
||||
<div class="action-bar-inner">
|
||||
|
||||
<button class="btn btn-back" type="button" disabled>
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M9.78 12.78a.75.75 0 01-1.06 0L4.47 8.53a.75.75 0 010-1.06l4.25-4.25a.75.75 0 011.06 1.06L6.06 8l3.72 3.72a.75.75 0 010 1.06z"/>
|
||||
</svg>
|
||||
<span data-i18n="wizard.button.back">Back</span>
|
||||
</button>
|
||||
|
||||
<button class="btn-save-draft" type="button" data-i18n="wizard.button.save_draft">Save draft</button>
|
||||
|
||||
<div class="action-bar-spacer"></div>
|
||||
|
||||
<span class="step-counter" data-step-counter>Step <strong>1</strong> of 5</span>
|
||||
|
||||
<button id="btn-continue" class="btn btn-continue" type="button">
|
||||
<span data-i18n="wizard.button.continue">Continue</span>
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M6.22 3.22a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06L9.94 8 6.22 4.28a.75.75 0 010-1.06z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /wizard-shell -->
|
||||
|
||||
<div class="modal-overlay" id="quick-secret-modal" data-testid="wizard-quick-secret-modal">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<span class="modal-title" data-i18n="wizard.step2.quick_secret_title">Quick create secret</span>
|
||||
<button class="modal-close" id="quick-secret-close-btn" type="button">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
|
||||
<line x1="1" y1="1" x2="11" y2="11"/><line x1="11" y1="1" x2="1" y2="11"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" style="display:grid; gap:14px;">
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="wizard.step2.quick_secret_name">Secret name</label>
|
||||
<input class="field-input" id="quick-secret-name" data-testid="wizard-quick-secret-name" type="text" autocomplete="off">
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="wizard.step2.quick_secret_kind">Secret kind</label>
|
||||
<select class="field-select" id="quick-secret-kind" data-testid="wizard-quick-secret-kind">
|
||||
<option value="token" data-i18n="secrets.kind.token">Token</option>
|
||||
<option value="header" data-i18n="secrets.kind.header">Header value</option>
|
||||
<option value="generic" data-i18n="secrets.kind.generic">Generic JSON</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field-group">
|
||||
<label class="field-label" data-i18n="wizard.step2.quick_secret_value">Secret value</label>
|
||||
<input class="field-input" id="quick-secret-value" data-testid="wizard-quick-secret-value" type="text" autocomplete="off">
|
||||
<div class="field-hint" data-i18n="wizard.step2.quick_secret_hint">The plaintext is encrypted immediately and will not be returned again.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" id="quick-secret-cancel-btn" type="button" data-i18n="btn.cancel">Cancel</button>
|
||||
<button class="btn-primary" id="quick-secret-submit-btn" data-testid="wizard-quick-secret-submit" type="button" data-i18n="wizard.step2.quick_secret_submit">Create secret</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="%CRANK_BUNDLE_WIZARD%"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,69 @@
|
||||
<!-- ════════════ STEP 1: Protocol ════════════ -->
|
||||
<div id="step-panel-1" class="step-pane">
|
||||
<div class="step-panel-header">
|
||||
<div class="step-panel-eyebrow">
|
||||
<div class="step-panel-eyebrow-line"></div>
|
||||
<span data-step-counter>Step 1 of 5</span>
|
||||
</div>
|
||||
<h1 class="step-panel-title" data-i18n="wizard.step1.title">Choose a protocol</h1>
|
||||
<p class="step-panel-subtitle" data-i18n="wizard.step1.subtitle">Select the transport protocol your upstream service uses. This determines how Crank will communicate with your API and which configuration options are available in subsequent steps.</p>
|
||||
</div>
|
||||
|
||||
<!-- Protocol selection cards -->
|
||||
<div class="protocol-grid" data-testid="wizard-protocol-grid">
|
||||
|
||||
<div class="protocol-card rest selected" data-protocol="rest" data-testid="wizard-protocol-rest" role="radio" aria-checked="true" tabindex="0">
|
||||
<div class="protocol-card-check">
|
||||
<svg viewBox="0 0 12 12"><polyline points="1.5,6 4.5,9 10.5,3"/></svg>
|
||||
</div>
|
||||
<div class="protocol-icon-wrap">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#58a6ff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
<path d="M3.6 9h16.8M3.6 15h16.8"/>
|
||||
<path d="M12 3a15.3 15.3 0 010 18M12 3a15.3 15.3 0 000 18"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="protocol-card-name" data-i18n="wizard.step1.rest_name">REST / HTTP</div>
|
||||
<div class="protocol-card-tagline" data-i18n="wizard.step1.rest_tagline">Standard HTTP verbs over a RESTful endpoint. The most broadly supported option.</div>
|
||||
<div class="protocol-card-tags">
|
||||
<span class="protocol-tag">GET</span>
|
||||
<span class="protocol-tag">POST</span>
|
||||
<span class="protocol-tag">PUT</span>
|
||||
<span class="protocol-tag">PATCH</span>
|
||||
<span class="protocol-tag">DELETE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-slot="wizard.protocol_cards.graphql"></div>
|
||||
<div data-slot="wizard.protocol_cards.grpc"></div>
|
||||
<div data-slot="wizard.protocol_cards.soap"></div>
|
||||
<div data-slot="wizard.protocol_cards.websocket"></div>
|
||||
|
||||
</div><!-- /protocol-grid -->
|
||||
|
||||
|
||||
<div class="info-callout" style="margin-bottom: 24px;">
|
||||
<svg class="info-callout-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="8,1 15,14 1,14" fill="none"/>
|
||||
<path d="M8 6v4M8 11.5v.5"/>
|
||||
</svg>
|
||||
<div class="info-callout-body">
|
||||
<div class="info-callout-title" data-i18n="wizard.step1.tip_title">You can change this later</div>
|
||||
<div class="info-callout-text">
|
||||
<span data-i18n="wizard.step1.tip_body">Switching protocol after configuring subsequent steps will clear schema and mapping data. Save your operation as a draft before experimenting. See protocol migration guide for details.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-callout" id="wizard-edition-protocol-note" hidden>
|
||||
<svg class="info-callout-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="8" cy="8" r="7"/>
|
||||
<path d="M8 11V8M8 5v-.5"/>
|
||||
</svg>
|
||||
<div class="info-callout-body">
|
||||
<div class="info-callout-title" data-i18n="wizard.step1.community_protocol_title">Community protocol scope</div>
|
||||
<div class="info-callout-text" id="wizard-edition-protocol-note-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /step-pane-1 -->
|
||||
@@ -0,0 +1,224 @@
|
||||
<!-- ════════════ STEP 2: Upstream target ════════════ -->
|
||||
<div id="step-panel-2" class="step-pane">
|
||||
<div class="step-panel-header">
|
||||
<div class="step-panel-eyebrow">
|
||||
<div class="step-panel-eyebrow-line"></div>
|
||||
<span data-step-counter>Step 2 of 5</span>
|
||||
</div>
|
||||
<h1 class="step-panel-title" data-i18n="wizard.step2.title">Select upstream & endpoint</h1>
|
||||
<p class="step-panel-subtitle" data-i18n="wizard.step2.subtitle">Choose an existing upstream — a shared host with its auth config — or register a new one. Then define the specific endpoint path this operation will call.</p>
|
||||
</div>
|
||||
|
||||
<!-- ── Upstream selector ── -->
|
||||
<div class="config-card" style="margin-bottom: 20px; overflow: visible; position: relative; z-index: 10;">
|
||||
<div class="config-card-header">
|
||||
<div class="config-card-header-icon">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="1" y="3" width="14" height="4" rx="1"/>
|
||||
<rect x="1" y="9" width="14" height="4" rx="1"/>
|
||||
<circle cx="4" cy="5" r="0.8" fill="var(--text-secondary)" stroke="none"/>
|
||||
<circle cx="4" cy="11" r="0.8" fill="var(--text-secondary)" stroke="none"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="config-card-title" data-i18n="wizard.step2.upstream_title">Upstream</div>
|
||||
<div class="config-card-subtitle" data-i18n="wizard.step2.upstream_subtitle">Shared host, base URL, and authentication</div>
|
||||
</div>
|
||||
<span class="config-card-optional" style="color: var(--error, #f87171);" data-i18n="wizard.required">Required</span>
|
||||
</div>
|
||||
<div class="config-card-body" style="gap: 12px; padding: 16px;">
|
||||
|
||||
<!-- Searchable combobox -->
|
||||
<div class="upstream-combobox" id="upstream-combobox">
|
||||
<div class="upstream-combobox-trigger" id="upstream-combobox-trigger" onclick="toggleUpstreamDropdown(event)">
|
||||
<div class="upstream-combobox-value" id="upstream-combobox-value">
|
||||
<span class="upstream-combobox-placeholder" data-i18n="wizard.step2.upstream_placeholder">Select an upstream…</span>
|
||||
</div>
|
||||
<svg class="upstream-combobox-chevron" width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="var(--text-muted)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 6l4 4 4-4"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Dropdown panel -->
|
||||
<div class="upstream-dropdown" id="upstream-dropdown" hidden>
|
||||
<div class="upstream-search-wrap">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-muted)" stroke-width="1.8" stroke-linecap="round">
|
||||
<circle cx="7" cy="7" r="5"/><path d="M12 12l2.5 2.5"/>
|
||||
</svg>
|
||||
<input class="upstream-search-input" id="upstream-search" type="text" data-i18n-ph="wizard.step2.search_placeholder" placeholder="Search by name or URL…" oninput="filterUpstreams(this.value)" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="upstream-dropdown-list" id="upstream-dropdown-list">
|
||||
<!-- populated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selected upstream preview -->
|
||||
<div class="upstream-preview" id="upstream-preview" hidden>
|
||||
<div class="upstream-preview-name" id="upstream-preview-name"></div>
|
||||
<div class="upstream-preview-url" id="upstream-preview-url"></div>
|
||||
<span class="upstream-auth-badge" id="upstream-preview-badge"></span>
|
||||
<button class="upstream-preview-change" onclick="beginEditSelectedUpstream(event)" data-i18n="wizard.step2.change">Change</button>
|
||||
</div>
|
||||
|
||||
<!-- Register new upstream trigger row -->
|
||||
<div class="upstream-new-trigger" id="upstream-new-trigger" onclick="startNewUpstream()">
|
||||
<div class="upstream-new-trigger-radio" id="upstream-new-trigger-radio">
|
||||
<div class="upstream-new-trigger-dot"></div>
|
||||
</div>
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="var(--accent)" stroke-width="2.2" stroke-linecap="round">
|
||||
<path d="M8 3v10M3 8h10"/>
|
||||
</svg>
|
||||
<span data-i18n="wizard.step2.register_new">Register new upstream</span>
|
||||
</div>
|
||||
|
||||
<!-- Register new upstream form (kept as-is per design) -->
|
||||
<div class="upstream-new-form" id="upstream-new-form" hidden>
|
||||
<div class="upstream-new-form-inner">
|
||||
<div class="form-row" style="grid-template-columns: 1fr 2fr;">
|
||||
<div class="form-group">
|
||||
<label class="form-label"><span data-i18n="wizard.step2.name">Name</span> <span class="form-label-required" data-i18n="workspace_setup.required">required</span></label>
|
||||
<input class="form-input" id="new-upstream-name" type="text" placeholder="e.g. acme-api" autocomplete="off" spellcheck="false">
|
||||
<div class="form-hint" data-i18n="wizard.step2.unique_hint">Unique identifier for this upstream.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label"><span data-i18n="wizard.step2.base_url">Base URL</span> <span class="form-label-required" data-i18n="workspace_setup.required">required</span></label>
|
||||
<input class="form-input input-mono" id="new-upstream-url" type="text" placeholder="https://api.example.com" autocomplete="off" spellcheck="false">
|
||||
<div class="form-hint" data-i18n="wizard.step2.base_url_hint">Root URL without a trailing slash. Auth is configured separately below.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step2.auth_selector">Upstream auth</label>
|
||||
<select class="form-select" id="new-upstream-auth-mode" data-testid="wizard-auth-mode-select" onchange="updateUpstreamAuthUi()">
|
||||
<option value="none" data-i18n="wizard.step2.auth_mode.none">No auth</option>
|
||||
<option value="existing" data-i18n="wizard.step2.auth_mode.existing">Use existing auth profile</option>
|
||||
<option value="create" data-i18n="wizard.step2.auth_mode.create">Create auth profile now</option>
|
||||
</select>
|
||||
<div class="form-hint" data-i18n="wizard.step2.auth_selector_hint">Use secrets-backed auth profiles instead of embedding credential headers in the upstream definition.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="upstream-auth-existing-group" hidden>
|
||||
<label class="form-label" data-i18n="wizard.step2.auth_profile">Auth profile</label>
|
||||
<select class="form-select" id="new-upstream-auth-profile" data-testid="wizard-auth-profile-select"></select>
|
||||
<div class="form-hint" id="new-upstream-auth-profile-hint" data-i18n="wizard.step2.auth_profile_hint">Selected profile will be resolved at runtime and attached through `execution_config.auth_profile_ref`.</div>
|
||||
</div>
|
||||
|
||||
<div class="config-card" id="upstream-auth-create-group" hidden style="margin-top: 6px;">
|
||||
<div class="config-card-header">
|
||||
<div>
|
||||
<div class="config-card-title" data-i18n="wizard.step2.create_profile_title">Create auth profile</div>
|
||||
<div class="config-card-subtitle" data-i18n="wizard.step2.create_profile_subtitle">Create a reusable profile backed by encrypted workspace secrets.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-card-body" style="gap: 14px;">
|
||||
<div class="form-group">
|
||||
<label class="form-label"><span data-i18n="wizard.step2.profile_name">Profile name</span> <span class="form-label-required" data-i18n="workspace_setup.required">required</span></label>
|
||||
<input class="form-input" id="new-auth-profile-name" type="text" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="form-row" style="grid-template-columns: 1fr 1fr;">
|
||||
<div class="form-group">
|
||||
<label class="form-label"><span data-i18n="wizard.step2.profile_kind">Auth kind</span> <span class="form-label-required" data-i18n="workspace_setup.required">required</span></label>
|
||||
<select class="form-select" id="new-auth-profile-kind" data-testid="wizard-auth-profile-kind-select" onchange="updateAuthProfileCreateUi()">
|
||||
<option value="bearer" data-i18n="wizard.step2.auth_kind.bearer">Bearer token</option>
|
||||
<option value="basic" data-i18n="wizard.step2.auth_kind.basic">Basic auth</option>
|
||||
<option value="api_key_header" data-i18n="wizard.step2.auth_kind.api_key_header">API key header</option>
|
||||
<option value="api_key_query" data-i18n="wizard.step2.auth_kind.api_key_query">API key query</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="auth-profile-header-name-group">
|
||||
<label class="form-label" data-i18n="wizard.step2.header_name">Header name</label>
|
||||
<input class="form-input input-mono" id="new-auth-profile-header-name" type="text" value="Authorization" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="auth-profile-query-param-group" hidden>
|
||||
<label class="form-label" data-i18n="wizard.step2.query_param">Query param</label>
|
||||
<input class="form-input input-mono" id="new-auth-profile-query-param" type="text" value="api_key" autocomplete="off" spellcheck="false">
|
||||
</div>
|
||||
<div class="form-row" id="auth-profile-basic-secret-row" hidden style="grid-template-columns: 1fr 1fr;">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step2.username_secret">Username secret</label>
|
||||
<select class="form-select" id="new-auth-profile-username-secret" data-testid="wizard-auth-username-secret-select"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step2.password_secret">Password secret</label>
|
||||
<select class="form-select" id="new-auth-profile-password-secret" data-testid="wizard-auth-password-secret-select"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="auth-profile-single-secret-group">
|
||||
<label class="form-label" data-i18n="wizard.step2.secret_value">Secret</label>
|
||||
<select class="form-select" id="new-auth-profile-secret-id" data-testid="wizard-auth-secret-select"></select>
|
||||
</div>
|
||||
<div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
|
||||
<button class="btn-ghost-sm" data-testid="wizard-open-quick-secret" onclick="openQuickSecretModal(event)" data-i18n="wizard.step2.quick_secret">Quick create secret</button>
|
||||
<a class="btn-ghost-sm" href="/secrets" target="_blank" rel="noopener" data-i18n="wizard.step2.manage_secrets">Open secrets page</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step2.static_headers">Static headers (optional)</label>
|
||||
<div class="code-block" style="border-radius: 6px;">
|
||||
<div class="code-toolbar">
|
||||
<div class="code-dots"><span></span><span></span><span></span></div>
|
||||
<span class="code-toolbar-label">json / static-headers</span>
|
||||
</div>
|
||||
<textarea class="form-textarea code-textarea" id="new-upstream-static-headers" rows="5">{
|
||||
}</textarea>
|
||||
</div>
|
||||
<div class="form-hint" data-i18n="wizard.step2.static_headers_hint">Optional non-secret headers sent on every request to this upstream. Use auth profiles for credentials.</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:8px; margin-top:4px;">
|
||||
<button class="btn-primary-sm" onclick="saveNewUpstream(event)" data-i18n="wizard.step2.save_upstream">Save upstream</button>
|
||||
<button class="btn-ghost-sm" onclick="cancelNewUpstream(event)" data-i18n="btn.cancel">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Endpoint ── -->
|
||||
<div class="config-card" style="margin-bottom: 20px;">
|
||||
<div class="config-card-header">
|
||||
<div class="config-card-header-icon">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2 8h12M10 4l4 4-4 4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="config-card-title" data-i18n="wizard.step2.endpoint_title">Endpoint</div>
|
||||
<div class="config-card-subtitle" data-i18n="wizard.step2.endpoint_subtitle">Path and HTTP method for this specific operation</div>
|
||||
</div>
|
||||
<span class="config-card-optional" style="color: var(--error, #f87171);" data-i18n="wizard.required">Required</span>
|
||||
</div>
|
||||
<div class="config-card-body" style="gap: 16px;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<span data-i18n="wizard.step2.path_template">Path template</span>
|
||||
<span class="form-label-required" data-i18n="workspace_setup.required">required</span>
|
||||
</label>
|
||||
<input class="form-input input-mono" id="endpoint-path" type="text" value="/v1/leads" autocomplete="off" spellcheck="false">
|
||||
<div class="form-hint" data-i18n="wizard.step2.path_hint">Appended to the upstream base URL. Use {param} for path variables.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /step-pane-2 -->
|
||||
|
||||
<!-- Template: upstream dropdown item -->
|
||||
<template id="tmpl-upstream-item">
|
||||
<div class="upstream-dropdown-item">
|
||||
<div class="upstream-dropdown-item-info">
|
||||
<div class="upstream-dropdown-item-name"></div>
|
||||
<div class="upstream-dropdown-item-url"></div>
|
||||
</div>
|
||||
<span class="upstream-auth-badge"></span>
|
||||
<svg class="upstream-dropdown-item-check" width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="var(--accent)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8l4 4 6-7"/></svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Template: upstream combobox trigger value -->
|
||||
<template id="tmpl-upstream-trigger-value">
|
||||
<div class="upstream-trigger-name"></div>
|
||||
<div class="upstream-trigger-url"></div>
|
||||
</template>
|
||||
@@ -0,0 +1,93 @@
|
||||
<!-- REST variant -->
|
||||
<div id="step-panel-3-rest" class="step-pane">
|
||||
<div class="step-panel-header">
|
||||
<div class="step-panel-eyebrow">
|
||||
<div class="step-panel-eyebrow-line"></div>
|
||||
<span data-step-counter>Step 3 of 5</span>
|
||||
</div>
|
||||
<h1 class="step-panel-title" data-i18n="wizard.step3.rest.title">HTTP method & format</h1>
|
||||
<p class="step-panel-subtitle" data-i18n="wizard.step3.rest.subtitle">Choose the HTTP verb this operation sends and configure content negotiation headers. Crank will serialize MCP tool arguments into the appropriate request body format.</p>
|
||||
</div>
|
||||
|
||||
<!-- HTTP method picker -->
|
||||
<div class="config-card" style="margin-bottom: 20px;">
|
||||
<div class="config-card-header">
|
||||
<div class="config-card-header-icon">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2 8h12M10 4l4 4-4 4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="config-card-title" data-i18n="wizard.step3.rest.method_title">HTTP method</div>
|
||||
<div class="config-card-subtitle" data-i18n="wizard.step3.rest.method_subtitle">Verb sent to the upstream on every tool invocation</div>
|
||||
</div>
|
||||
<span class="config-card-optional" style="color:var(--error,#f87171);" data-i18n="wizard.required">Required</span>
|
||||
</div>
|
||||
<div class="config-card-body">
|
||||
<div class="method-grid">
|
||||
<button class="method-card" data-method="GET" onclick="selectMethod(this)">
|
||||
<span class="method-name">GET</span>
|
||||
<span class="method-desc" data-i18n="wizard.step3.rest.read">Read</span>
|
||||
</button>
|
||||
<button class="method-card active" data-method="POST" onclick="selectMethod(this)">
|
||||
<span class="method-name">POST</span>
|
||||
<span class="method-desc" data-i18n="wizard.step3.rest.create">Create</span>
|
||||
</button>
|
||||
<button class="method-card" data-method="PUT" onclick="selectMethod(this)">
|
||||
<span class="method-name">PUT</span>
|
||||
<span class="method-desc" data-i18n="wizard.step3.rest.replace">Replace</span>
|
||||
</button>
|
||||
<button class="method-card" data-method="PATCH" onclick="selectMethod(this)">
|
||||
<span class="method-name">PATCH</span>
|
||||
<span class="method-desc" data-i18n="wizard.step3.rest.update">Update</span>
|
||||
</button>
|
||||
<button class="method-card" data-method="DELETE" onclick="selectMethod(this)">
|
||||
<span class="method-name">DELETE</span>
|
||||
<span class="method-desc" data-i18n="wizard.step3.rest.remove">Remove</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="method-callout" id="method-callout-rest" hidden></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Request format -->
|
||||
<div class="config-card" style="margin-bottom: 20px;">
|
||||
<div class="config-card-header">
|
||||
<div class="config-card-header-icon">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="3" width="12" height="10" rx="1.5"/>
|
||||
<path d="M5 7h6M5 10h4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="config-card-title" data-i18n="wizard.step3.rest.format_title">Request format</div>
|
||||
<div class="config-card-subtitle" data-i18n="wizard.step3.rest.format_subtitle">Content negotiation and serialisation</div>
|
||||
</div>
|
||||
<span class="config-card-optional" data-i18n="wizard.optional">Optional</span>
|
||||
</div>
|
||||
<div class="config-card-body" style="gap: 16px;">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step3.rest.content_type">Content-Type</label>
|
||||
<select class="form-select">
|
||||
<option selected>application/json</option>
|
||||
<option>application/x-www-form-urlencoded</option>
|
||||
<option>multipart/form-data</option>
|
||||
<option>text/plain</option>
|
||||
</select>
|
||||
<div class="form-hint" data-i18n="wizard.step3.rest.content_type_hint">How the request body is encoded when sent to the upstream.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step3.rest.accept">Accept</label>
|
||||
<select class="form-select">
|
||||
<option selected>application/json</option>
|
||||
<option>text/plain</option>
|
||||
<option>*/*</option>
|
||||
</select>
|
||||
<div class="form-hint" data-i18n="wizard.step3.rest.accept_hint">Accepted response content types from the upstream server.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /step-pane-3-rest -->
|
||||
@@ -0,0 +1,158 @@
|
||||
<div id="step-panel-4" class="step-pane">
|
||||
<div class="step-panel-header">
|
||||
<div class="step-panel-eyebrow">
|
||||
<div class="step-panel-eyebrow-line"></div>
|
||||
<span data-step-counter>Step 4 of 5</span>
|
||||
</div>
|
||||
<h1 class="step-panel-title" data-i18n="wizard.step4.title">Tool config</h1>
|
||||
<p class="step-panel-subtitle" data-i18n="wizard.step4.subtitle">Name your tool, write the LLM-facing description, and define the input/output schemas. The description is the primary signal the model uses when deciding whether to call this tool.</p>
|
||||
</div>
|
||||
|
||||
<div class="config-card" style="margin-bottom: 20px;">
|
||||
<div class="config-card-header">
|
||||
<div class="config-card-header-icon">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2 5h12M2 8h8M2 11h5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="config-card-title" data-i18n="wizard.step4.identity_title">Tool identity</div>
|
||||
<div class="config-card-subtitle" data-i18n="wizard.step4.identity_subtitle">Machine-readable name and LLM-facing description</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-card-body" style="gap: 20px;">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<span data-i18n="wizard.step4.tool_name">Tool name</span>
|
||||
<span class="form-label-required" data-i18n="workspace_setup.required">required</span>
|
||||
</label>
|
||||
<input id="tool-name" class="form-input input-mono" type="text" value="create_crm_lead" autocomplete="off" spellcheck="false">
|
||||
<div class="form-hint" data-i18n="wizard.step4.tool_name_hint">Machine-readable identifier used in MCP tool calls. Only lowercase letters, numbers and underscores. Cannot be changed after publishing.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<span data-i18n="wizard.step4.display_name">Display name</span>
|
||||
<span class="form-label-required" data-i18n="workspace_setup.required">required</span>
|
||||
</label>
|
||||
<input id="tool-display-name" class="form-input" type="text" value="Create CRM Lead" autocomplete="off">
|
||||
<div class="form-hint" data-i18n="wizard.step4.display_name_hint">Human-readable name shown in the console and audit log.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<span data-i18n="wizard.step4.tool_title">Tool title</span>
|
||||
<span class="form-label-optional" data-i18n="workspace_setup.optional">(optional)</span>
|
||||
</label>
|
||||
<input id="tool-title" class="form-input" type="text" value="Create a new CRM lead record" autocomplete="off">
|
||||
<div class="form-hint" data-i18n="wizard.step4.tool_title_hint">Short imperative sentence shown in the MCP tool manifest.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">
|
||||
<span data-i18n="wizard.step4.description">Description</span>
|
||||
<span class="form-label-required" data-i18n="workspace_setup.required">required</span>
|
||||
</label>
|
||||
<textarea id="tool-description" class="form-textarea" rows="5">Creates a new lead record in the CRM from the provided contact details. Use this tool when the user wants to add a new prospect or contact. Returns the new lead ID and creation timestamp on success.</textarea>
|
||||
<div class="form-hint" data-i18n="wizard.step4.description_hint">LLM-facing description. Be precise — this is the primary signal the model uses to decide whether to invoke this tool.</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-callout" style="margin-bottom: 24px;">
|
||||
<svg class="info-callout-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="8" cy="8" r="7"/>
|
||||
<path d="M8 11V8M8 5v-.5"/>
|
||||
</svg>
|
||||
<div class="info-callout-body">
|
||||
<div class="info-callout-title" data-i18n="wizard.step4.description_title">Writing effective descriptions</div>
|
||||
<div class="info-callout-text" data-i18n="wizard.step4.description_body">Start with a verb ("Creates", "Fetches", "Updates"). Mention key input requirements. Describe what a successful response looks like. Avoid vague phrasing like "handles" or "manages". See description best practices →</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-card" style="margin-bottom: 20px;">
|
||||
<div class="config-card-header">
|
||||
<div class="config-card-header-icon">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 2h8a2 2 0 012 2v8a2 2 0 01-2 2H4a2 2 0 01-2-2V4a2 2 0 012-2z"/>
|
||||
<path d="M6 6l-2 2 2 2M10 6l2 2-2 2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="config-card-title" data-i18n="wizard.step4.input_schema_title">Input schema</div>
|
||||
<div class="config-card-subtitle" data-i18n="wizard.step4.input_schema_subtitle">Parameters the LLM passes when calling this tool</div>
|
||||
</div>
|
||||
<span class="config-card-optional" data-i18n="wizard.step4.schema_draft">JSON Schema draft-07</span>
|
||||
</div>
|
||||
<div class="config-card-body" style="gap: 0; padding: 0;">
|
||||
<div class="code-block" style="border-radius: 0; border: none;">
|
||||
<div class="code-toolbar">
|
||||
<div class="code-dots"><span></span><span></span><span></span></div>
|
||||
<span class="code-toolbar-label">json / input-schema</span>
|
||||
</div>
|
||||
<textarea id="tool-input-schema" class="form-textarea code-textarea" rows="16">{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"required": ["first_name", "last_name", "email"],
|
||||
"properties": {
|
||||
"first_name": { "type": "string", "description": "Contact first name" },
|
||||
"last_name": { "type": "string", "description": "Contact last name" },
|
||||
"email": { "type": "string", "format": "email", "description": "Primary contact email address" },
|
||||
"company": { "type": "string", "description": "Company or organisation name" },
|
||||
"phone": { "type": "string", "description": "Phone number in E.164 format" },
|
||||
"source": { "type": "string", "enum": ["web","api","import"], "default": "api" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-card" style="margin-bottom: 20px;">
|
||||
<div class="config-card-header">
|
||||
<div class="config-card-header-icon">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 2h8a2 2 0 012 2v8a2 2 0 01-2 2H4a2 2 0 01-2-2V4a2 2 0 012-2z"/>
|
||||
<path d="M5 8h6M5 5h4M5 11h3"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="config-card-title" data-i18n="wizard.step4.output_schema_title">Output schema</div>
|
||||
<div class="config-card-subtitle" data-i18n="wizard.step4.output_schema_subtitle">Shape of the data returned to the LLM after a successful call</div>
|
||||
</div>
|
||||
<span class="config-card-optional" data-i18n="wizard.step4.schema_draft">JSON Schema draft-07</span>
|
||||
</div>
|
||||
<div class="config-card-body" style="gap: 0; padding: 0;">
|
||||
<div class="code-block" style="border-radius: 0; border: none;">
|
||||
<div class="code-toolbar">
|
||||
<div class="code-dots"><span></span><span></span><span></span></div>
|
||||
<span class="code-toolbar-label">json / output-schema</span>
|
||||
</div>
|
||||
<textarea id="tool-output-schema" class="form-textarea code-textarea" rows="14">{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"lead_id": { "type": "string", "description": "Unique identifier of the created lead" },
|
||||
"status": { "type": "string", "description": "Initial lead status assigned by the CRM" },
|
||||
"created_at": { "type": "string", "format": "date-time" },
|
||||
"owner_id": { "type": "string", "description": "CRM user ID of the assigned owner" }
|
||||
}
|
||||
}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-callout" style="margin-bottom: 24px;">
|
||||
<svg class="info-callout-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="8" cy="8" r="7"/>
|
||||
<path d="M8 11V8M8 5v-.5"/>
|
||||
</svg>
|
||||
<div class="info-callout-body">
|
||||
<div class="info-callout-title" data-i18n="wizard.step4.protocol_agnostic_title">Schemas are protocol-agnostic</div>
|
||||
<div class="info-callout-text" data-i18n="wizard.step4.protocol_agnostic_body">These schemas describe the MCP contract, not the upstream wire format. Field mapping in Step 5 translates between the two. You can upload a sample JSON response to auto-generate the output schema.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /step-pane-4 -->
|
||||
@@ -0,0 +1,265 @@
|
||||
<div id="step-panel-5" class="step-pane">
|
||||
<div class="step-panel-header">
|
||||
<div class="step-panel-eyebrow">
|
||||
<div class="step-panel-eyebrow-line"></div>
|
||||
<span data-step-counter>Step 5 of 5</span>
|
||||
</div>
|
||||
<h1 class="step-panel-title" data-i18n="wizard.step5.title">Mapping and execution</h1>
|
||||
<p class="step-panel-subtitle" data-i18n="wizard.step5.subtitle">Define how MCP tool arguments map to upstream request fields, and how the upstream response maps back to MCP output. Then set execution parameters — auth is configured per-upstream in step 2.</p>
|
||||
</div>
|
||||
|
||||
<div class="section-divider" style="margin-bottom: 16px;">
|
||||
<span class="section-divider-label" data-i18n="wizard.step5.input_request">Input → Request</span>
|
||||
<div class="section-divider-line"></div>
|
||||
</div>
|
||||
|
||||
<div class="config-card" style="margin-bottom: 20px;">
|
||||
<div class="config-card-body" style="gap: 0; padding: 0;">
|
||||
<div class="code-block" style="border-radius: 0; border: none;">
|
||||
<div class="code-toolbar">
|
||||
<div class="code-dots"><span></span><span></span><span></span></div>
|
||||
<span class="code-toolbar-label">yaml / input-mapping</span>
|
||||
</div>
|
||||
<textarea id="tool-input-mapping" class="form-textarea code-textarea" rows="12">first_name: "$.input.first_name"
|
||||
last_name: "$.input.last_name"
|
||||
email: "$.input.email"
|
||||
company: "$.input.company"
|
||||
phone: "$.input.phone"
|
||||
source: "$.input.source"</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-divider" style="margin-bottom: 16px;">
|
||||
<span class="section-divider-label" data-i18n="wizard.step5.output_response">Response → Output</span>
|
||||
<div class="section-divider-line"></div>
|
||||
</div>
|
||||
|
||||
<div class="config-card" style="margin-bottom: 20px;">
|
||||
<div class="config-card-body" style="gap: 0; padding: 0;">
|
||||
<div class="code-block" style="border-radius: 0; border: none;">
|
||||
<div class="code-toolbar">
|
||||
<div class="code-dots"><span></span><span></span><span></span></div>
|
||||
<span class="code-toolbar-label">yaml / output-mapping</span>
|
||||
</div>
|
||||
<textarea id="tool-output-mapping" class="form-textarea code-textarea" rows="10">lead_id: "$.response.data.id"
|
||||
status: "$.response.data.status"
|
||||
created_at: "$.response.data.created_at"
|
||||
owner_id: "$.response.data.owner.id"</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-divider" style="margin-bottom: 16px;">
|
||||
<span class="section-divider-label" data-i18n="wizard.step5.execution">Execution</span>
|
||||
<div class="section-divider-line"></div>
|
||||
</div>
|
||||
|
||||
<div class="config-card" style="margin-bottom: 20px;">
|
||||
<div class="config-card-header">
|
||||
<div class="config-card-header-icon">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="8" cy="8" r="6"/>
|
||||
<path d="M8 5v3l2 2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="config-card-title" data-i18n="wizard.step5.exec_title">Execution config</div>
|
||||
<div class="config-card-subtitle" data-i18n="wizard.step5.exec_subtitle">Timeouts, retry policy and auth profile reference</div>
|
||||
</div>
|
||||
<span class="config-card-optional" data-i18n="wizard.optional">Optional</span>
|
||||
</div>
|
||||
<div class="config-card-body" style="gap: 0; padding: 0;">
|
||||
<div class="code-block" style="border-radius: 0; border: none;">
|
||||
<div class="code-toolbar">
|
||||
<div class="code-dots"><span></span><span></span><span></span></div>
|
||||
<span class="code-toolbar-label">yaml / exec-config</span>
|
||||
</div>
|
||||
<textarea id="tool-exec-config" class="form-textarea code-textarea" rows="10">timeout_ms: 10000
|
||||
retry:
|
||||
max_attempts: 3
|
||||
backoff_ms: 500
|
||||
auth:
|
||||
profile: acme_bearer
|
||||
tls:
|
||||
verify: true</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-callout" id="wizard-security-level-note" hidden style="margin-bottom: 20px;">
|
||||
<svg class="info-callout-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="8" cy="8" r="7"/>
|
||||
<path d="M8 11V8M8 5v-.5"/>
|
||||
</svg>
|
||||
<div class="info-callout-body">
|
||||
<div class="info-callout-title" data-i18n="wizard.step5.security_level_title">Operation security in Community</div>
|
||||
<div class="info-callout-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-divider" style="margin-bottom: 16px;">
|
||||
<span class="section-divider-label" data-i18n="wizard.step5.live_title">Live validation and publishing</span>
|
||||
<div class="section-divider-line"></div>
|
||||
</div>
|
||||
|
||||
<div id="wizard-live-status" class="info-callout" hidden style="margin-bottom: 20px;">
|
||||
<svg class="info-callout-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="8" cy="8" r="7"/>
|
||||
<path d="M8 11V8M8 5v-.5"/>
|
||||
</svg>
|
||||
<div class="info-callout-body">
|
||||
<div class="info-callout-title" id="wizard-live-status-title" data-i18n="wizard.step5.draft_actions">Draft actions</div>
|
||||
<div class="info-callout-text" id="wizard-live-status-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-callout info-callout-subtle" style="margin-bottom: 20px;">
|
||||
<svg class="info-callout-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="8" cy="8" r="7"/>
|
||||
<path d="M8 11V8M8 5v-.5"/>
|
||||
</svg>
|
||||
<div class="info-callout-body">
|
||||
<div class="info-callout-title" data-i18n="wizard.step5.live_save_title">Live actions save the draft first</div>
|
||||
<div class="info-callout-text" data-i18n="wizard.step5.live_save_body">Sample uploads, test runs, YAML actions and publish all persist the current draft before calling the backend.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-card" style="margin-bottom: 20px;">
|
||||
<div class="config-card-header">
|
||||
<div class="config-card-header-icon">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="3" width="12" height="10" rx="1.5"/>
|
||||
<path d="M5 7h6M5 10h6"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="config-card-title" data-i18n="wizard.step5.samples_title">Samples and draft generation</div>
|
||||
<div class="config-card-subtitle" data-i18n="wizard.step5.samples_subtitle">Persist input/output samples, then generate schemas and mappings from real payloads.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-card-body" style="gap: 16px;">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step5.input_sample">Input sample</label>
|
||||
<textarea id="wizard-input-sample" class="form-textarea code-textarea" rows="9" spellcheck="false">{
|
||||
"first_name": "Ada",
|
||||
"last_name": "Lovelace",
|
||||
"email": "ada@example.com"
|
||||
}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step5.output_sample">Output sample</label>
|
||||
<textarea id="wizard-output-sample" class="form-textarea code-textarea" rows="9" spellcheck="false">{
|
||||
"id": "lead_123",
|
||||
"status": "created"
|
||||
}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap: 8px; flex-wrap: wrap;">
|
||||
<button id="wizard-upload-input-sample" class="btn-primary-sm" type="button" data-i18n="wizard.step5.save_input_sample">Save input sample</button>
|
||||
<button id="wizard-upload-output-sample" class="btn-ghost-sm" type="button" data-i18n="wizard.step5.save_output_sample">Save output sample</button>
|
||||
<button id="wizard-generate-draft" class="btn-ghost-sm" type="button" data-i18n="wizard.step5.generate_draft">Generate draft from samples</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-card" style="margin-bottom: 20px;">
|
||||
<div class="config-card-header">
|
||||
<div class="config-card-header-icon">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2 8h12M10 4l4 4-4 4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="config-card-title" data-i18n="wizard.step5.test_title">Test run</div>
|
||||
<div class="config-card-subtitle" data-i18n="wizard.step5.test_subtitle">Run the current draft against the upstream before publishing it.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-card-body" style="gap: 16px;">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step5.test_input_payload">Test input payload</label>
|
||||
<textarea id="wizard-test-input" class="form-textarea code-textarea" rows="8" spellcheck="false">{
|
||||
"first_name": "Ada",
|
||||
"last_name": "Lovelace",
|
||||
"email": "ada@example.com"
|
||||
}</textarea>
|
||||
</div>
|
||||
<div style="display:flex; gap: 8px; flex-wrap: wrap;">
|
||||
<button id="wizard-run-test" class="btn-primary-sm" type="button" data-i18n="wizard.step5.run_test">Run test</button>
|
||||
<button id="wizard-copy-test-response" class="btn-ghost-sm" type="button" data-i18n="wizard.step5.use_response_output">Use response as output sample</button>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step5.request_preview">Request preview</label>
|
||||
<textarea id="wizard-test-request-preview" class="form-textarea code-textarea" rows="8" spellcheck="false" readonly></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step5.response_preview">Response preview</label>
|
||||
<textarea id="wizard-test-response-preview" class="form-textarea code-textarea" rows="8" spellcheck="false" readonly></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step5.errors">Errors</label>
|
||||
<textarea id="wizard-test-errors" class="form-textarea code-textarea" rows="4" spellcheck="false" readonly></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-card" style="margin-bottom: 20px;">
|
||||
<div class="config-card-header">
|
||||
<div class="config-card-header-icon">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M8 2v12M3 7l5-5 5 5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="config-card-title" data-i18n="wizard.step5.yaml_title">YAML import and export</div>
|
||||
<div class="config-card-subtitle" data-i18n="wizard.step5.yaml_subtitle">Move operation drafts between environments without leaving the wizard.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-card-body" style="gap: 16px;">
|
||||
<div style="display:flex; gap: 8px; flex-wrap: wrap;">
|
||||
<button id="wizard-export-yaml" class="btn-ghost-sm" type="button" data-i18n="wizard.step5.export_yaml">Export YAML</button>
|
||||
<button id="wizard-import-yaml-file-trigger" class="btn-ghost-sm" type="button" data-i18n="wizard.step5.load_yaml_file">Load YAML file</button>
|
||||
<button id="wizard-import-yaml" class="btn-primary-sm" type="button" data-i18n="wizard.step5.import_yaml">Import YAML</button>
|
||||
<input id="wizard-import-yaml-file" type="file" accept=".yaml,.yml,text/yaml" style="display:none">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="wizard.step5.yaml_document">YAML document</label>
|
||||
<textarea id="wizard-import-yaml-text" class="form-textarea code-textarea" rows="10" spellcheck="false" data-i18n-ph="wizard.step5.yaml_placeholder" placeholder="Paste exported YAML here to create or upsert an operation."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-card" style="margin-bottom: 20px;">
|
||||
<div class="config-card-header">
|
||||
<div class="config-card-header-icon">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-secondary)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 8l3 3 7-7"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="config-card-title" data-i18n="wizard.step5.publish_title">Publish</div>
|
||||
<div class="config-card-subtitle" data-i18n="wizard.step5.publish_subtitle">Save the current draft and expose it to published agents.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-card-body" style="gap: 12px;">
|
||||
<div class="form-hint" data-i18n="wizard.step5.publish_hint">Publishing uses the current draft version from this wizard session.</div>
|
||||
<div style="display:flex; gap: 8px; flex-wrap: wrap;">
|
||||
<button id="wizard-publish-operation" class="btn-primary-sm" type="button" data-i18n="wizard.step5.publish_action">Publish operation</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-callout" style="margin-bottom: 24px;">
|
||||
<svg class="info-callout-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="8" cy="8" r="7"/>
|
||||
<path d="M8 11V8M8 5v-.5"/>
|
||||
</svg>
|
||||
<div class="info-callout-body">
|
||||
<div class="info-callout-title" data-i18n="wizard.step5.editable_title">Operation stays editable in this wizard</div>
|
||||
<div class="info-callout-text" data-i18n="wizard.step5.editable_body">Saving creates or updates the current draft in place. Use the live actions above to upload samples, test the draft, export or import YAML, and publish when the upstream contract is ready.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /step-pane-5 -->
|
||||
@@ -0,0 +1,322 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<base href="../">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Crank — Workspace</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/variables.css">
|
||||
<link rel="stylesheet" href="css/layout.css">
|
||||
<link rel="stylesheet" href="css/pages.css">
|
||||
<link rel="stylesheet" href="css/wizard.css">
|
||||
<link rel="stylesheet" href="css/workspace-setup.css">
|
||||
<script src="%CRANK_BUNDLE_PROTECTED_CORE%"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="ws-setup-page">
|
||||
|
||||
<!-- Top bar -->
|
||||
<div class="ws-setup-topbar">
|
||||
<a href="/" class="ws-setup-logo">
|
||||
<div class="ws-setup-logo-mark">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" width="18" height="18" fill="white"><path fill-rule="evenodd" d="M 10.000,1.800 L 12.287,3.962 L 15.408,3.557 L 15.987,6.650 L 18.750,8.157 L 17.400,11.000 L 18.750,13.843 L 15.987,15.350 L 15.408,18.443 L 12.287,18.038 L 10.000,20.200 L 7.713,18.038 L 4.592,18.443 L 4.013,15.350 L 1.250,13.843 L 2.600,11.000 L 1.250,8.157 L 4.013,6.650 L 4.592,3.557 L 7.713,3.962 Z M 12.800,11.000 A 2.8 2.8 0 1 0 7.200,11.000 A 2.8 2.8 0 1 0 12.800,11.000 Z"/><path fill-rule="evenodd" d="M 11.791,15.604 Q 16.083,17.179 21.375,21.154 A 2.00 2.00 0 1 1 23.625,17.846 Q 19.782,15.610 14.940,10.973 Z M 24.300,19.500 A 1.8 1.8 0 1 0 20.700,19.500 A 1.8 1.8 0 1 0 24.300,19.500 Z"/></svg>
|
||||
</div>
|
||||
Crank
|
||||
</a>
|
||||
<a href="javascript:history.back()" class="ws-setup-back">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10 4l-4 4 4 4"/></svg>
|
||||
<span data-i18n="workspace_setup.back">Back</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="ws-setup-body">
|
||||
<div class="ws-setup-container">
|
||||
|
||||
<div class="ws-setup-header">
|
||||
<div class="ws-setup-title" id="page-title" data-i18n="workspace_setup.title">Workspace settings</div>
|
||||
<div class="ws-setup-subtitle" id="page-subtitle" data-i18n="workspace_setup.page_subtitle">Configure your workspace identity, default settings, and team members.</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Identity ── -->
|
||||
<div class="ws-form-section">
|
||||
<div class="ws-form-section-title" data-i18n="workspace_setup.identity.title">Workspace identity</div>
|
||||
|
||||
<div class="ws-avatar-row">
|
||||
<div class="ws-avatar-preview" id="ws-avatar-preview">A</div>
|
||||
<div>
|
||||
<div class="ws-avatar-hint" data-i18n="workspace_setup.identity.avatar_hint">Avatar is derived from your workspace name.</div>
|
||||
<div class="ws-color-swatches">
|
||||
<div class="ws-color-swatch active" style="background:#0d9488;" data-color="#0d9488" onclick="pickColor(this)" title="Teal"></div>
|
||||
<div class="ws-color-swatch" style="background:#7c3aed;" data-color="#7c3aed" onclick="pickColor(this)" title="Purple"></div>
|
||||
<div class="ws-color-swatch" style="background:#0891b2;" data-color="#0891b2" onclick="pickColor(this)" title="Cyan"></div>
|
||||
<div class="ws-color-swatch" style="background:#d29922;" data-color="#d29922" onclick="pickColor(this)" title="Amber"></div>
|
||||
<div class="ws-color-swatch" style="background:#1a7f37;" data-color="#1a7f37" onclick="pickColor(this)" title="Green"></div>
|
||||
<div class="ws-color-swatch" style="background:#cf222e;" data-color="#cf222e" onclick="pickColor(this)" title="Red"></div>
|
||||
<div class="ws-color-swatch" style="background:#5e6ad2;" data-color="#5e6ad2" onclick="pickColor(this)" title="Indigo"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom:14px;">
|
||||
<label class="form-label"><span data-i18n="workspace_setup.name">Workspace name</span> <span class="form-label-required" data-i18n="workspace_setup.required">required</span></label>
|
||||
<input class="form-input" id="ws-name" type="text" placeholder="Acme Inc" autocomplete="off" oninput="onWsNameInput(this.value)">
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom:14px;">
|
||||
<label class="form-label"><span data-i18n="workspace_setup.slug">Slug</span> <span class="form-label-required" data-i18n="workspace_setup.required">required</span></label>
|
||||
<div style="position:relative;">
|
||||
<span style="position:absolute;left:12px;top:50%;transform:translateY(-50%);font-size:13px;color:var(--text-muted);font-family:monospace;pointer-events:none;">mcp.crank.io/</span>
|
||||
<input class="form-input input-mono" id="ws-slug" type="text" placeholder="acme-inc" autocomplete="off" style="padding-left: 112px;" oninput="onWsSlugInput(this.value)">
|
||||
</div>
|
||||
<div class="form-hint" data-i18n="workspace_setup.slug_hint">Only lowercase letters, numbers, and hyphens. Used in MCP endpoint URLs.</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label"><span data-i18n="workspace_setup.description">Description</span> <span class="form-label-optional" data-i18n="workspace_setup.optional">(optional)</span></label>
|
||||
<textarea class="form-textarea" id="ws-desc" rows="2" data-i18n-ph="workspace_setup.description_placeholder" placeholder="What does this workspace do?"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Default settings ── -->
|
||||
<div class="ws-form-section">
|
||||
<div class="ws-form-section-title" data-i18n="workspace_setup.defaults.title">Default settings</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="workspace_setup.protocol">Default protocol</label>
|
||||
<select class="form-input" id="ws-protocol">
|
||||
<option value="rest" selected>REST</option>
|
||||
</select>
|
||||
<div class="form-hint" data-i18n="workspace_setup.protocol_hint">Pre-selected in the operation wizard.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-i18n="workspace_setup.timezone">Timezone</label>
|
||||
<select class="form-input" id="ws-timezone">
|
||||
<option value="UTC" selected>UTC</option>
|
||||
<option value="America/New_York">Eastern Time</option>
|
||||
<option value="America/Chicago">Central Time</option>
|
||||
<option value="America/Los_Angeles">Pacific Time</option>
|
||||
<option value="Europe/London">London</option>
|
||||
<option value="Europe/Berlin">Berlin / Paris</option>
|
||||
<option value="Asia/Tokyo">Tokyo</option>
|
||||
<option value="Asia/Shanghai">Shanghai</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top:4px;margin-bottom:0;">
|
||||
<label class="form-label" data-i18n="workspace_setup.language">Interface language</label>
|
||||
<div class="lang-switcher">
|
||||
<button class="lang-btn active" data-lang="en" onclick="setLang('en')">
|
||||
<span class="lang-flag">🇬🇧</span>
|
||||
<span data-i18n="settings.lang.en">English</span>
|
||||
</button>
|
||||
<button class="lang-btn" data-lang="ru" onclick="setLang('ru')">
|
||||
<span class="lang-flag">🇷🇺</span>
|
||||
<span data-i18n="settings.lang.ru">Русский</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Members (edit mode only) ── -->
|
||||
<div id="section-members" class="ws-form-section" hidden style="padding:0; overflow:hidden;">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:20px 24px 16px;">
|
||||
<div>
|
||||
<div class="ws-form-section-title" style="margin-bottom:2px;" data-i18n="workspace_setup.members.title">Team members</div>
|
||||
<div style="font-size:12px;color:var(--text-muted);" id="members-count-label">4 members</div>
|
||||
</div>
|
||||
<button class="btn-primary" type="button" style="padding:7px 14px;font-size:13px;" onclick="toggleInviteForm()">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" style="margin-right:4px;"><path d="M7.75 2a.75.75 0 01.75.75V7h4.25a.75.75 0 010 1.5H8.5v4.25a.75.75 0 01-1.5 0V8.5H2.75a.75.75 0 010-1.5H7V2.75A.75.75 0 017.75 2z"/></svg>
|
||||
<span data-i18n="workspace_setup.members.invite">Invite member</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Invite form -->
|
||||
<div id="invite-form" hidden style="padding: 0 24px 16px; border-bottom: 1px solid var(--border-subtle);">
|
||||
<div style="display:flex; gap:10px; align-items:flex-end; flex-wrap:wrap;">
|
||||
<div class="form-group" style="flex:1; min-width:200px; margin-bottom:0;">
|
||||
<label class="form-label" data-i18n="workspace_setup.members.email">Email address</label>
|
||||
<input class="form-input" id="invite-email" type="email" placeholder="colleague@company.com" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group" style="width:140px; margin-bottom:0;">
|
||||
<label class="form-label" data-i18n="workspace_setup.members.role">Role</label>
|
||||
<select class="form-input" id="invite-role">
|
||||
<option value="admin" data-i18n="workspace_setup.role.admin">Admin</option>
|
||||
<option value="developer" selected data-i18n="workspace_setup.role.operator">Developer</option>
|
||||
<option value="viewer" data-i18n="workspace_setup.role.viewer">Viewer</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn-primary" type="button" style="padding:7px 14px;font-size:13px;" onclick="sendInvite()" data-i18n="workspace_setup.members.send_invite">Send invite</button>
|
||||
<button class="btn-ghost-sm" type="button" style="padding:7px 12px;" onclick="toggleInviteForm()" data-i18n="workspace_setup.members.cancel">Cancel</button>
|
||||
</div>
|
||||
<div id="invite-success" hidden style="margin-top:10px; font-size:13px; color: #3fb950;" data-i18n="workspace_setup.members.invite_sent">✓ Invite sent.</div>
|
||||
</div>
|
||||
|
||||
<!-- Members list -->
|
||||
<div style="padding: 4px 24px 8px;" id="members-list">
|
||||
<div class="member-row">
|
||||
<div class="member-avatar" style="background:linear-gradient(135deg,#0d9488,#6366f1);">AT</div>
|
||||
<div class="member-info">
|
||||
<div class="member-name">Crank <span style="font-size:11px;font-weight:400;color:var(--text-muted);" data-i18n="workspace_setup.members.you">(you)</span></div>
|
||||
<div class="member-email">operator@acme.com</div>
|
||||
</div>
|
||||
<div class="member-last-active">Today</div>
|
||||
<span class="member-role-badge role-owner" data-i18n="workspace_setup.role.owner">Owner</span>
|
||||
</div>
|
||||
<div class="member-row">
|
||||
<div class="member-avatar" style="background:linear-gradient(135deg,#7c3aed,#4f46e5);">BS</div>
|
||||
<div class="member-info">
|
||||
<div class="member-name">Blake Smith</div>
|
||||
<div class="member-email">blake@acme.com</div>
|
||||
</div>
|
||||
<div class="member-last-active">2 days ago</div>
|
||||
<select class="member-role-select" onchange="updateRole(this, 'blake@acme.com')">
|
||||
<option value="admin" selected data-i18n="workspace_setup.role.admin">Admin</option>
|
||||
<option value="developer" data-i18n="workspace_setup.role.operator">Developer</option>
|
||||
<option value="viewer" data-i18n="workspace_setup.role.viewer">Viewer</option>
|
||||
</select>
|
||||
<button class="member-remove-btn" onclick="removeMember(this, 'Blake Smith')" data-i18n-title="workspace_setup.members.remove" title="Remove member">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 4h12M5 4V3a1 1 0 011-1h4a1 1 0 011 1v1M10 8v4M6 8v4"/><path d="M3 4l1 9a1 1 0 001 1h6a1 1 0 001-1l1-9"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="member-row">
|
||||
<div class="member-avatar" style="background:linear-gradient(135deg,#d29922,#f59e0b);">CL</div>
|
||||
<div class="member-info">
|
||||
<div class="member-name">Carol Lane</div>
|
||||
<div class="member-email">carol@acme.com</div>
|
||||
</div>
|
||||
<div class="member-last-active">1 week ago</div>
|
||||
<select class="member-role-select" onchange="updateRole(this, 'carol@acme.com')">
|
||||
<option value="admin" data-i18n="workspace_setup.role.admin">Admin</option>
|
||||
<option value="developer" selected data-i18n="workspace_setup.role.operator">Developer</option>
|
||||
<option value="viewer" data-i18n="workspace_setup.role.viewer">Viewer</option>
|
||||
</select>
|
||||
<button class="member-remove-btn" onclick="removeMember(this, 'Carol Lane')" data-i18n-title="workspace_setup.members.remove" title="Remove member">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 4h12M5 4V3a1 1 0 011-1h4a1 1 0 011 1v1M10 8v4M6 8v4"/><path d="M3 4l1 9a1 1 0 001 1h6a1 1 0 001-1l1-9"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="member-row">
|
||||
<div class="member-avatar" style="background:linear-gradient(135deg,#0891b2,#06b6d4);">DM</div>
|
||||
<div class="member-info">
|
||||
<div class="member-name">Dan Morgan</div>
|
||||
<div class="member-email">dan@acme.com</div>
|
||||
</div>
|
||||
<div class="member-last-active">Never</div>
|
||||
<select class="member-role-select" onchange="updateRole(this, 'dan@acme.com')">
|
||||
<option value="admin" data-i18n="workspace_setup.role.admin">Admin</option>
|
||||
<option value="developer" data-i18n="workspace_setup.role.operator">Developer</option>
|
||||
<option value="viewer" selected data-i18n="workspace_setup.role.viewer">Viewer</option>
|
||||
</select>
|
||||
<button class="member-remove-btn" onclick="removeMember(this, 'Dan Morgan')" data-i18n-title="workspace_setup.members.remove" title="Remove member">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 4h12M5 4V3a1 1 0 011-1h4a1 1 0 011 1v1M10 8v4M6 8v4"/><path d="M3 4l1 9a1 1 0 001 1h6a1 1 0 001-1l1-9"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pending invites -->
|
||||
<div id="pending-invites" hidden style="border-top: 1px solid var(--border-subtle); padding: 12px 24px 16px;">
|
||||
<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-muted);margin-bottom:10px;">
|
||||
<span data-i18n="workspace_setup.members.pending">Pending invites</span> <span id="pending-count" style="background:rgba(139,148,158,0.15);border-radius:10px;padding:1px 7px;font-size:10px;">1</span>
|
||||
</div>
|
||||
<div class="member-row" id="invite-john">
|
||||
<div class="member-avatar" style="background:var(--bg-canvas);border:1.5px dashed var(--border);color:var(--text-muted);font-size:16px;">?</div>
|
||||
<div class="member-info">
|
||||
<div class="member-name">john@startup.com</div>
|
||||
<div class="member-email">Invited 3 days ago · not yet accepted</div>
|
||||
</div>
|
||||
<div class="member-last-active">—</div>
|
||||
<span class="member-role-badge" style="background:rgba(139,148,158,0.12);color:var(--text-muted);border:1px solid var(--border);" data-i18n="workspace_setup.role.operator">Developer</span>
|
||||
<button class="member-remove-btn" onclick="revokeInvite(this)" data-i18n-title="workspace_setup.members.revoke" title="Revoke invite">
|
||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="2" y1="2" x2="14" y2="14"/><line x1="14" y1="2" x2="2" y2="14"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Roles reference -->
|
||||
<div style="border-top:1px solid var(--border-subtle);padding:16px 24px 20px;">
|
||||
<div style="font-size:12px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:14px;" data-i18n="workspace_setup.roles.title">Roles & permissions</div>
|
||||
<div class="role-def-row">
|
||||
<span class="member-role-badge role-owner" style="min-width:80px;text-align:center;" data-i18n="workspace_setup.role.owner">Owner</span>
|
||||
<div class="role-def-text" data-i18n="workspace_setup.roles.owner_body">Full workspace control, billing access, can transfer or delete the workspace.</div>
|
||||
</div>
|
||||
<div class="role-def-row">
|
||||
<span class="member-role-badge role-admin" style="min-width:80px;text-align:center;" data-i18n="workspace_setup.role.admin">Admin</span>
|
||||
<div class="role-def-text" data-i18n="workspace_setup.roles.admin_body">Manage members and roles, create and edit all operations and agents, manage API keys.</div>
|
||||
</div>
|
||||
<div class="role-def-row">
|
||||
<span class="member-role-badge role-developer" style="min-width:80px;text-align:center;" data-i18n="workspace_setup.role.operator">Developer</span>
|
||||
<div class="role-def-text" data-i18n="workspace_setup.roles.developer_body">Create and edit operations and agents. Cannot manage members or workspace settings.</div>
|
||||
</div>
|
||||
<div class="role-def-row">
|
||||
<span class="member-role-badge role-viewer" style="min-width:80px;text-align:center;" data-i18n="workspace_setup.role.viewer">Viewer</span>
|
||||
<div class="role-def-text" data-i18n="workspace_setup.roles.viewer_body">Read-only access to operations, agents, and logs.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Invite team members (create mode only) ── -->
|
||||
<div id="section-invite" class="ws-form-section" hidden>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px;">
|
||||
<div class="ws-form-section-title" style="margin-bottom:0;"><span data-i18n="workspace_setup.invite.title">Invite team members</span> <span style="font-size:12px;font-weight:400;color:var(--text-muted);" data-i18n="workspace_setup.optional">(optional)</span></div>
|
||||
<button class="btn-ghost-sm" type="button" onclick="addInviteRow()" style="font-size:12px;padding:4px 10px;" data-i18n="workspace_setup.invite.add_person">+ Add person</button>
|
||||
</div>
|
||||
<div class="form-hint" style="margin-bottom:14px;" data-i18n="workspace_setup.invite.later">You can invite more people later from Workspace settings.</div>
|
||||
<div class="invite-rows" id="invite-rows">
|
||||
<div class="invite-row">
|
||||
<input class="form-input" type="email" placeholder="colleague@company.com" autocomplete="off" style="flex:1;margin-bottom:0;">
|
||||
<select class="form-input" style="width:130px;margin-bottom:0;">
|
||||
<option value="admin" data-i18n="workspace_setup.role.admin">Admin</option>
|
||||
<option value="developer" selected data-i18n="workspace_setup.role.operator">Developer</option>
|
||||
<option value="viewer" data-i18n="workspace_setup.role.viewer">Viewer</option>
|
||||
</select>
|
||||
<button class="invite-row-remove" onclick="removeInviteRow(this)" data-i18n-title="workspace_setup.members.remove" title="Remove">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><line x1="1" y1="1" x2="11" y2="11"/><line x1="11" y1="1" x2="1" y2="11"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Actions ── -->
|
||||
<div class="ws-setup-actions">
|
||||
<a href="javascript:history.back()" class="btn-ghost-sm" style="padding:9px 18px;font-size:13px;text-decoration:none;color:var(--text-secondary);" data-i18n="btn.cancel">Cancel</a>
|
||||
<button class="btn-primary ws-submit-btn" id="submit-btn" type="button" onclick="submitForm()" data-i18n="workspace_setup.actions.save">Save changes</button>
|
||||
</div>
|
||||
|
||||
<div class="ws-setup-footer-note" id="footer-note" hidden>
|
||||
<span data-i18n="workspace_setup.create.footer">You'll be the Owner of this workspace. You can invite additional members after creation.</span>
|
||||
</div>
|
||||
|
||||
<!-- ── Danger zone (edit mode only) ── -->
|
||||
<div id="section-danger" hidden style="margin-top:32px;">
|
||||
<div class="ws-form-section" style="border-color: rgba(248,81,73,0.25); padding:0; overflow:hidden;">
|
||||
<div style="padding:16px 24px; border-bottom: 1px solid rgba(248,81,73,0.15);">
|
||||
<div style="font-size:14px;font-weight:600;color:var(--red);" data-i18n="workspace_setup.danger.title">Danger zone</div>
|
||||
</div>
|
||||
<div class="danger-zone-action">
|
||||
<div class="danger-zone-text">
|
||||
<div class="danger-zone-title" data-i18n="workspace_setup.danger.export_title">Export all data</div>
|
||||
<div class="danger-zone-desc" data-i18n="workspace_setup.danger.export_body">Download a JSON snapshot of workspace settings, memberships, invitations, operations, agents and platform API keys.</div>
|
||||
</div>
|
||||
<button class="btn-danger" id="export-workspace-btn" type="button" data-i18n="workspace_setup.export">Export</button>
|
||||
</div>
|
||||
<div class="danger-zone-action">
|
||||
<div class="danger-zone-text">
|
||||
<div class="danger-zone-title" data-i18n="workspace_setup.danger.delete_title">Delete workspace</div>
|
||||
<div class="danger-zone-desc" data-i18n="workspace_setup.danger.delete_body">Permanently deletes all operations, keys, logs and settings. This action cannot be undone. You will be logged out immediately.</div>
|
||||
</div>
|
||||
<button class="btn-danger" id="delete-workspace-btn" type="button" data-i18n="workspace_setup.delete">Delete workspace</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="%CRANK_BUNDLE_WORKSPACE_SETUP%"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user