feat: migrate alpine ui into app shell

This commit is contained in:
a.tolmachev
2026-03-30 00:15:02 +03:00
parent f33aa29c88
commit 7070231c3e
84 changed files with 12530 additions and 92 deletions
+380
View File
@@ -0,0 +1,380 @@
<!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>try{if(!localStorage.getItem('crank_user'))window.location.replace('login.html');}catch(e){}</script>
<script src="js/config.js"></script>
<script src="js/i18n.js"></script>
<script src="js/workspace.js"></script>
</head>
<body x-data="agents">
<!-- ═══════════════════ NAVBAR ═══════════════════ -->
<nav class="navbar">
<a href="index.html" 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" style="display:none">
<div class="ws-dropdown-label">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.html?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>
Create workspace
</button>
</div>
</div>
</div>
<div class="nav-links">
<a href="index.html" class="nav-link" data-i18n="nav.operations">Operations</a>
<a href="html/agents.html" class="nav-link active" data-i18n="nav.agents">Agents</a>
<a href="html/api-keys.html" class="nav-link" data-i18n="nav.apikeys">API Keys</a>
<a href="html/logs.html" class="nav-link" data-i18n="nav.logs">Logs</a>
<a href="html/usage.html" class="nav-link" data-i18n="nav.usage">Usage</a>
</div>
<div class="nav-right">
<button class="nav-hamburger" aria-label="Menu"><span></span><span></span><span></span></button>
<div class="nav-kbd">
<svg width="12" height="12"><use href="icons/general/grid.svg#icon"/></svg>
<span>⌘K</span>
</div>
<button class="nav-icon-btn" 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'" 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">Operator</div>
<div class="user-dropdown-role" id="user-ws-role">Admin · acme-workspace</div>
</div>
<button class="user-dropdown-item" onclick="window.location.href='settings.html'">
<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="localStorage.removeItem('crank_user'); window.location.href='login.html'">
<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">
<a href="index.html" class="mobile-nav-link" data-i18n="nav.operations">Operations</a>
<a href="html/agents.html" class="mobile-nav-link active" data-i18n="nav.agents">Agents</a>
<a href="html/api-keys.html" class="mobile-nav-link" data-i18n="nav.apikeys">API Keys</a>
<a href="html/logs.html" class="mobile-nav-link" data-i18n="nav.logs">Logs</a>
<a href="html/usage.html" 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">Agents</h1>
<p class="page-subheading">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>
New agent
</button>
</div>
<!-- Stats -->
<div class="stats-row">
<div class="stat-card">
<div class="stat-label">Total agents</div>
<div class="stat-value" x-text="agents.length"></div>
<div class="stat-delta">across this workspace</div>
</div>
<div class="stat-card">
<div class="stat-label">Active</div>
<div class="stat-value" x-text="activeCount"></div>
<div class="stat-delta" x-text="agents.length ? Math.round(activeCount/agents.length*100)+'% of total' : ''"></div>
</div>
<div class="stat-card">
<div class="stat-label">Operations exposed</div>
<div class="stat-value" x-text="totalOpsExposed"></div>
<div class="stat-delta" x-text="'across ' + agents.length + ' agents'"></div>
</div>
<div class="stat-card">
<div class="stat-label">Calls today</div>
<div class="stat-value" x-text="formatCalls(totalCalls)"></div>
<div class="stat-delta up">all agents combined</div>
</div>
</div>
<!-- Search + filter bar -->
<div class="filter-bar" style="margin-bottom:0;">
<div class="search-box">
<svg width="14" height="14"><use href="icons/general/search.svg#icon"/></svg>
<input
type="text"
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="filteredAgents.length"></span> of <span x-text="agents.length"></span> agents
</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>Each agent gets its own MCP endpoint. Connect your LLM client to a specific agent so it only sees the tools it needs — not all <strong x-text="operations.length">?</strong> operations in this workspace.</span>
</div>
<!-- Loading -->
<div class="loading-spinner" x-show="loading">
<div class="spinner"></div>
<span>Loading agents…</span>
</div>
<!-- Empty state: no agents at all -->
<div class="empty-state" x-show="!loading && 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">No agents yet</div>
<div class="empty-state-sub">Create your first agent to get a dedicated MCP endpoint with a curated set of tools.</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>
New agent
</button>
</div>
<!-- Empty state: search returned nothing -->
<div class="empty-state" x-show="!loading && agents.length > 0 && filteredAgents.length === 0">
<div class="empty-state-title">No agents match "<span x-text="agentSearch"></span>"</div>
<div class="empty-state-sub">Try a different search term.</div>
</div>
<!-- Agents grid -->
<div class="agents-grid" x-show="!loading && 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="agent.status.charAt(0).toUpperCase() + agent.status.slice(1)"></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>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>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>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="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>
<!-- Footer -->
<div class="agent-card-footer">
<span class="agent-card-date" x-text="'Created ' + 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>
Edit
</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>
Delete
</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' ? 'New agent' : 'Edit agent'"></div>
<div class="drawer-subtitle" x-text="drawerMode === 'create' ? 'Configure a named MCP endpoint for your LLM' : 'Update agent settings and tool selection'"></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">Identity</div>
<div class="form-group" style="margin-bottom: 14px;">
<label class="form-label">Display name <span class="form-label-required">required</span></label>
<input class="form-input" type="text" 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">Slug <span class="form-label-required">required</span></label>
<input class="form-input input-mono" type="text" placeholder="customer-support" autocomplete="off"
:value="form.slug" @input="onSlugInput($event.target.value)">
<div class="form-hint" x-show="form.slug">
MCP endpoint: <code style="font-family:monospace;font-size:11px;color:var(--accent)" x-text="'/mcp/v1/' + (localStorage.getItem('crank_workspace') || 'acme-workspace') + '/' + form.slug"></code>
</div>
</div>
<div class="form-group" style="margin-bottom: 14px;">
<label class="form-label">Description <span class="form-label-optional">(optional)</span></label>
<textarea class="form-textarea" rows="3" 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">Status</label>
<div class="agent-status-toggle">
<button class="agent-status-opt" :class="{ active: form.status === 'active' }" @click="form.status = 'active'">
<span class="status-dot" style="background: var(--success, #3fb950)"></span> Active
</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> Draft
</button>
</div>
</div>
</div>
<!-- Operations picker section -->
<div class="drawer-section">
<div class="drawer-section-header">
<div class="drawer-section-title">Operations</div>
<span class="drawer-section-count" x-text="form.selectedOps.length + ' selected'"></span>
</div>
<div class="drawer-section-sub">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" 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">
No operations match "<span x-text="opSearch"></span>"
</div>
</div>
<!-- Footer count -->
<div class="ops-picker-footer" x-show="form.selectedOps.length > 0">
<span x-text="form.selectedOps.length"></span> operation<span x-show="form.selectedOps.length !== 1">s</span> selected
<button @click="form.selectedOps = []" style="background:none;border:none;color:var(--text-muted);font-size:12px;cursor:pointer;margin-left:8px;">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>You've selected <strong x-text="form.selectedOps.length"></strong> tools. LLMs perform best with &lt;15 tools per agent. Consider splitting into separate agents by use-case.</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()">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' ? 'Create agent' : 'Save changes'">
Create agent
</button>
</div>
</div><!-- /drawer -->
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
<script src="js/agents.js"></script>
</body>
</html>
+281
View File
@@ -0,0 +1,281 @@
<!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 — API 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); }
</style>
<script>try{if(!localStorage.getItem('crank_user'))window.location.replace('login.html');}catch(e){}</script>
<script src="js/config.js"></script>
<script src="js/i18n.js"></script>
<script src="js/workspace.js"></script>
</head>
<body>
<!-- ═══════════════════ NAVBAR ═══════════════════ -->
<nav class="navbar">
<a href="index.html" 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" style="display:none">
<div class="ws-dropdown-label">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.html?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>
Create workspace
</button>
</div>
</div>
</div>
<div class="nav-links">
<a href="index.html" class="nav-link" data-i18n="nav.operations">Operations</a>
<a href="html/agents.html" class="nav-link" data-i18n="nav.agents">Agents</a>
<a href="html/api-keys.html" class="nav-link active" data-i18n="nav.apikeys">API Keys</a>
<a href="html/logs.html" class="nav-link" data-i18n="nav.logs">Logs</a>
<a href="html/usage.html" class="nav-link" data-i18n="nav.usage">Usage</a>
</div>
<div class="nav-right">
<button class="nav-hamburger" aria-label="Menu"><span></span><span></span><span></span></button>
<div class="nav-kbd">
<svg width="12" height="12"><use href="icons/general/grid.svg#icon"/></svg>
<span>⌘K</span>
</div>
<button class="nav-icon-btn" 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" title="Account">AT</div>
<div class="user-dropdown">
<div class="user-dropdown-header">
<div class="user-dropdown-name">Operator</div>
<div class="user-dropdown-role">Admin · acme-workspace</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">
<a href="index.html" class="mobile-nav-link" data-i18n="nav.operations">Operations</a>
<a href="html/agents.html" class="mobile-nav-link" data-i18n="nav.agents">Agents</a>
<a href="html/api-keys.html" class="mobile-nav-link active" data-i18n="nav.apikeys">API Keys</a>
<a href="html/logs.html" class="mobile-nav-link" data-i18n="nav.logs">Logs</a>
<a href="html/usage.html" 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">API Keys</h1>
<p class="page-subtitle">Keys authenticate external clients and CI pipelines to the Crank API.</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>
New key
</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>Keys are only shown once.</strong> Copy and store them securely immediately after creation — Crank stores only a hash. Revoked keys cannot be restored.
</div>
</div>
<div class="section-card">
<div class="section-card-header">
<div>
<div class="section-card-title">Active keys</div>
<div class="section-card-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" placeholder="Filter keys…" autocomplete="off">
</div>
</div>
<div id="keys-table-wrap">
<table class="data-table" id="keys-table">
<thead>
<tr>
<th>Name</th>
<th>Key prefix</th>
<th>Scopes</th>
<th>Created</th>
<th>Last used</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody id="keys-tbody">
</tbody>
</table>
</div>
</div>
<div class="section-card">
<div class="section-card-header">
<div class="section-card-title">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;">List operations, read configs, query logs and usage metrics.</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;">Create, update, and delete operations and their configurations.</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;">Publish draft operations, rollback deployments, and manage traffic splits.</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">Create API 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">Key name</label>
<input class="field-input" id="new-key-name" type="text" placeholder="e.g. Production, CI pipeline" autocomplete="off">
<div class="field-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">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" style="display:none;">
<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>Copy this key now.</strong> It won't be shown again.</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" 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">Cancel</button>
<button class="btn-primary" id="modal-confirm-btn" type="button">Create key</button>
</div>
<div class="modal-footer" id="modal-footer-done" style="display:none;">
<button class="btn-primary" id="modal-done-btn" type="button">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" 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" 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" style="display:none;">
<button class="btn-icon" 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="js/nav.js"></script>
<script src="js/api-keys.js"></script>
</body>
</html>
+13
View File
@@ -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>
+78
View File
@@ -0,0 +1,78 @@
<!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="js/config.js"></script>
<script src="js/i18n.js"></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">Sign in</div>
<div class="login-sub">Welcome back to your workspace</div>
<div class="login-error" id="login-error">
Invalid email or password. Please try again.
</div>
<form id="login-form" novalidate>
<div class="field">
<label class="field-label" for="email">Email address</label>
<input class="field-input" type="email" id="email" placeholder="you@acme.com" autocomplete="email" autofocus>
</div>
<div class="field">
<label class="field-label" for="password">Password</label>
<input class="field-input" type="password" id="password" placeholder="••••••••" autocomplete="current-password">
<div class="field-footer">
<a href="#" class="field-link">Forgot password?</a>
</div>
</div>
<button class="btn-signin" type="submit">Sign in</button>
</form>
<div class="login-divider">
<div class="login-divider-line"></div>
<span class="login-divider-text">or continue with</span>
<div class="login-divider-line"></div>
</div>
<button class="btn-sso" type="button">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z" fill="#FBBC05"/>
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
</svg>
Google SSO
</button>
</div>
<div class="login-footer">
Don't have an account? <a href="#">Request access</a>
</div>
<div class="version-badge">Crank v0.9.0-beta</div>
</div>
</div>
<script src="js/login.js"></script>
</body>
</html>
+148
View File
@@ -0,0 +1,148 @@
<!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>try{if(!localStorage.getItem('crank_user'))window.location.replace('login.html');}catch(e){}</script>
<script src="js/config.js"></script>
<script src="js/i18n.js"></script>
<script src="js/workspace.js"></script>
</head>
<body>
<nav class="navbar">
<a href="index.html" 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" style="display:none">
<div class="ws-dropdown-label">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.html?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>
Create workspace
</button>
</div>
</div>
</div>
<div class="nav-links">
<a href="index.html" class="nav-link" data-i18n="nav.operations">Operations</a>
<a href="html/agents.html" class="nav-link" data-i18n="nav.agents">Agents</a>
<a href="html/api-keys.html" class="nav-link" data-i18n="nav.apikeys">API Keys</a>
<a href="html/logs.html" class="nav-link active" data-i18n="nav.logs">Logs</a>
<a href="html/usage.html" class="nav-link" data-i18n="nav.usage">Usage</a>
</div>
<div class="nav-right">
<button class="nav-hamburger" aria-label="Menu"><span></span><span></span><span></span></button>
<div class="nav-kbd">
<svg width="12" height="12"><use href="icons/general/grid.svg#icon"/></svg>
<span>⌘K</span>
</div>
<button class="nav-icon-btn" 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" title="Account">AT</div>
<div class="user-dropdown">
<div class="user-dropdown-header">
<div class="user-dropdown-name">Operator</div>
<div class="user-dropdown-role">Admin · acme-workspace</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">
<a href="index.html" class="mobile-nav-link" data-i18n="nav.operations">Operations</a>
<a href="html/agents.html" class="mobile-nav-link" data-i18n="nav.agents">Agents</a>
<a href="html/api-keys.html" class="mobile-nav-link" data-i18n="nav.apikeys">API Keys</a>
<a href="html/logs.html" class="mobile-nav-link active" data-i18n="nav.logs">Logs</a>
<a href="html/usage.html" 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">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">Live</span>
<div class="toolbar-sep"></div>
<select class="time-range-select" id="time-range">
<option value="30m">Last 30 min</option>
<option value="1h" selected>Last 1 hour</option>
<option value="6h">Last 6 hours</option>
<option value="24h">Last 24 hours</option>
<option value="7d">Last 7 days</option>
</select>
<div class="toolbar-sep"></div>
<button class="filter-chip active" data-level="all" id="chip-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" 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>
Refresh
</button>
</div>
<div class="log-list" id="log-list">
</div>
</div>
</div>
<script src="js/nav.js"></script>
<script src="js/logs.js"></script>
</body>
</html>
+597
View File
@@ -0,0 +1,597 @@
<!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>try{if(!localStorage.getItem('crank_user'))window.location.replace('login.html');}catch(e){}</script>
<script src="js/config.js"></script>
<script src="js/i18n.js"></script>
<script src="js/workspace.js"></script>
</head>
<body>
<nav class="navbar">
<a href="index.html" 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" style="display:none">
<div class="ws-dropdown-label">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.html?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>
Create workspace
</button>
</div>
</div>
</div>
<div class="nav-links">
<a href="index.html" class="nav-link" data-i18n="nav.operations">Operations</a>
<a href="html/agents.html" class="nav-link" data-i18n="nav.agents">Agents</a>
<a href="html/api-keys.html" class="nav-link" data-i18n="nav.apikeys">API Keys</a>
<a href="html/logs.html" class="nav-link" data-i18n="nav.logs">Logs</a>
<a href="html/usage.html" class="nav-link" data-i18n="nav.usage">Usage</a>
</div>
<div class="nav-right">
<button class="nav-hamburger" aria-label="Menu"><span></span><span></span><span></span></button>
<div class="nav-kbd">
<svg width="12" height="12"><use href="icons/general/grid.svg#icon"/></svg>
<span>⌘K</span>
</div>
<button class="nav-icon-btn" 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" title="Account">AT</div>
<div class="user-dropdown">
<div class="user-dropdown-header">
<div class="user-dropdown-name">Operator</div>
<div class="user-dropdown-role">Admin · acme-workspace</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">
<a href="index.html" class="mobile-nav-link" data-i18n="nav.operations">Operations</a>
<a href="html/agents.html" class="mobile-nav-link" data-i18n="nav.agents">Agents</a>
<a href="html/api-keys.html" class="mobile-nav-link" data-i18n="nav.apikeys">API Keys</a>
<a href="html/logs.html" class="mobile-nav-link" data-i18n="nav.logs">Logs</a>
<a href="html/usage.html" 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">Account settings</h1>
<p class="page-subtitle">Manage your personal profile, security, and notification preferences.</p>
</div>
</div>
<div class="settings-layout">
<!-- ── Sidebar nav ── -->
<nav class="settings-nav" id="settings-nav">
<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" style="display:none;">
<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" type="text" value="acme-workspace" autocomplete="off">
<div class="field-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" type="text" value="Acme Corp" autocomplete="off">
</div>
<div class="field-group">
<label class="field-label">Description <span style="font-size:11px;color:var(--text-muted);">(optional)</span></label>
<textarea class="field-textarea" rows="2" 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">
<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">
<option selected>REST / HTTP</option>
<option>GraphQL</option>
<option>gRPC</option>
</select>
</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" 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">Operator</div>
<div class="avatar-sub" id="profile-email">operator@acme-workspace</div>
<button class="btn-secondary" type="button" style="padding:5px 12px;font-size:12.5px;">Change photo</button>
</div>
</div>
<div class="field-hint" style="margin-bottom:16px;">Avatar will appear in the console and team views.</div>
<div class="field-row">
<div class="field-group">
<label class="field-label">First name</label>
<input class="field-input" id="field-firstname" type="text" value="Operator" autocomplete="given-name">
</div>
<div class="field-group">
<label class="field-label">Last name</label>
<input class="field-input" id="field-lastname" type="text" value="" autocomplete="family-name" placeholder="Optional">
</div>
</div>
<div class="field-group">
<label class="field-label">Email address</label>
<input class="field-input" id="field-email" type="email" value="operator@acme.com" autocomplete="email">
<div class="field-hint">Changing email requires re-verification. You'll receive a confirmation link.</div>
</div>
<div style="display:flex;justify-content:flex-end;">
<button class="btn-primary" type="button">Save profile</button>
</div>
</div>
</div>
</div>
<!-- ■ MEMBERS — moved to workspace-setup.html ■ -->
<div id="section-members" style="display:none;">
<!-- Members card -->
<div class="section-card" style="margin-bottom: 16px;">
<div class="section-card-header">
<div>
<div class="section-card-title">Team members</div>
<div class="section-card-subtitle" 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>
Invite member
</button>
</div>
<!-- Invite form -->
<div id="invite-form" style="display:none; padding: 0 20px 16px; border-bottom: 1px solid var(--border-subtle);">
<div style="display:flex; gap:10px; align-items:flex-end; flex-wrap:wrap;">
<div class="field-group" style="flex:1; min-width:200px; margin-bottom:0;">
<label class="field-label">Email address</label>
<input class="field-input" id="invite-email" type="email" placeholder="colleague@company.com" autocomplete="off">
</div>
<div class="field-group" style="width:150px; margin-bottom:0;">
<label class="field-label">Role</label>
<select class="field-input" id="invite-role">
<option value="admin">Admin</option>
<option value="developer" selected>Developer</option>
<option value="viewer">Viewer</option>
</select>
</div>
<button class="btn-primary" type="button" style="padding:7px 14px;font-size:13px;white-space:nowrap;" onclick="sendInvite()">Send invite</button>
<button class="btn-secondary" type="button" style="padding:7px 12px;font-size:13px;" onclick="toggleInviteForm()">Cancel</button>
</div>
<div id="invite-success" style="display:none; margin-top:10px; font-size:13px; color: #3fb950;">
✓ Invite sent successfully.
</div>
</div>
<!-- Members list -->
<div class="section-card-body" style="padding: 4px 20px 8px;" id="members-list">
<!-- Owner row (you) -->
<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">Operator <span style="font-size:11px;font-weight:400;color:var(--text-muted);">(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">Owner</span>
</div>
<!-- Admin -->
<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>Admin</option>
<option value="developer">Developer</option>
<option value="viewer">Viewer</option>
</select>
<button class="member-remove-btn" onclick="removeMember(this, 'Blake Smith')" 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>
<!-- Developer -->
<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">Admin</option>
<option value="developer" selected>Developer</option>
<option value="viewer">Viewer</option>
</select>
<button class="member-remove-btn" onclick="removeMember(this, 'Carol Lane')" 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>
<!-- Viewer -->
<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">Admin</option>
<option value="developer">Developer</option>
<option value="viewer" selected>Viewer</option>
</select>
<button class="member-remove-btn" onclick="removeMember(this, 'Dan Morgan')" 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" style="border-top: 1px solid var(--border-subtle); padding: 12px 20px 16px;">
<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:var(--text-muted);margin-bottom:10px;">Pending invites <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);">Developer</span>
<button class="member-remove-btn" onclick="revokeInvite(this)" 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>
</div>
<!-- Roles & permissions reference -->
<div class="section-card" style="margin-bottom: 20px;">
<div class="section-card-header">
<div class="section-card-title">Roles &amp; permissions</div>
</div>
<div class="section-card-body" style="padding: 4px 0 8px; gap: 0;">
<div class="role-def-row">
<span class="member-role-badge role-owner" style="min-width:80px;text-align:center;">Owner</span>
<div class="role-def-text">Full workspace control, billing access, can transfer or delete the workspace. Only one owner per workspace.</div>
</div>
<div class="role-def-row">
<span class="member-role-badge role-admin" style="min-width:80px;text-align:center;">Admin</span>
<div class="role-def-text">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;">Developer</span>
<div class="role-def-text">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;">Viewer</span>
<div class="role-def-text">Read-only access to operations, agents, and logs. Cannot create or modify anything.</div>
</div>
</div>
</div>
</div>
<script>
function toggleInviteForm() {
var f = document.getElementById('invite-form');
var s = document.getElementById('invite-success');
if (f.style.display === 'none') {
f.style.display = '';
s.style.display = 'none';
document.getElementById('invite-email').focus();
} else {
f.style.display = 'none';
}
}
function sendInvite() {
var email = document.getElementById('invite-email').value.trim();
if (!email || !email.includes('@')) return;
document.getElementById('invite-success').style.display = '';
document.getElementById('invite-email').value = '';
}
function updateRole(select, email) {
// In production: API call to update role
console.log('Role updated:', email, '->', select.value);
}
function removeMember(btn, name) {
if (!confirm('Remove ' + name + ' from this workspace?')) return;
btn.closest('.member-row').remove();
updateMembersCount();
}
function revokeInvite(btn) {
btn.closest('.member-row').remove();
var pending = document.getElementById('pending-invites');
if (pending && !pending.querySelector('.member-row')) {
pending.style.display = 'none';
}
}
function updateMembersCount() {
var rows = document.querySelectorAll('#members-list .member-row');
var label = document.getElementById('members-count-label');
if (label) label.textContent = rows.length + ' member' + (rows.length !== 1 ? 's' : '');
}
</script>
<!-- ■ 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">Security</div>
</div>
<div class="section-card-body" style="padding-bottom:8px;">
<div class="field-group">
<label class="field-label">Current password</label>
<input class="field-input" type="password" placeholder="••••••••" autocomplete="current-password">
</div>
<div class="field-row">
<div class="field-group">
<label class="field-label">New password</label>
<input class="field-input" type="password" placeholder="min. 12 characters" autocomplete="new-password">
</div>
<div class="field-group">
<label class="field-label">Confirm new password</label>
<input class="field-input" type="password" placeholder="••••••••" autocomplete="new-password">
</div>
</div>
<div style="display:flex;justify-content:flex-end;padding-bottom:8px;">
<button class="btn-primary" type="button">Change password</button>
</div>
</div>
<div style="padding: 4px 20px 16px;">
<div class="settings-section-divider" style="margin-top:0;"></div>
<div style="font-size:13.5px;font-weight:600;color:var(--text-primary);margin-bottom:12px;">Two-factor authentication</div>
<div class="toggle-setting-row">
<div class="toggle-setting-text">
<div class="toggle-setting-label">TOTP authenticator app</div>
<div class="toggle-setting-desc">Use Google Authenticator, 1Password, or any TOTP-compatible app.</div>
</div>
<span class="badge badge-active" style="margin-right:8px;">Enabled</span>
<button class="btn-secondary" type="button" style="padding:5px 12px;font-size:12.5px;">Configure</button>
</div>
<div class="toggle-setting-row">
<div class="toggle-setting-text">
<div class="toggle-setting-label">Hardware security key</div>
<div class="toggle-setting-desc">FIDO2 / WebAuthn passkey or YubiKey.</div>
</div>
<button class="btn-secondary" type="button" style="padding:5px 12px;font-size:12.5px;">Add key</button>
</div>
</div>
</div>
<div class="section-card" style="margin-bottom: 20px;">
<div class="section-card-header">
<div class="section-card-title">Active sessions</div>
</div>
<div style="padding: 8px 20px 16px;">
<div class="member-row">
<div class="member-info">
<div class="member-name">Chrome · macOS — <span style="color:var(--green);font-size:12px;">current</span></div>
<div class="member-email">Last active: now · 93.184.216.34 (US)</div>
</div>
</div>
<div class="member-row">
<div class="member-info">
<div class="member-name">Safari · iPhone</div>
<div class="member-email">Last active: 2 days ago · 93.184.216.34 (US)</div>
</div>
<button class="btn-icon danger" title="Revoke session">
<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="member-row">
<div class="member-info">
<div class="member-name">Firefox · Linux</div>
<div class="member-email">Last active: 8 days ago · 172.16.0.4 (Internal)</div>
</div>
<button class="btn-icon danger" title="Revoke session">
<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>
</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">Notification preferences</div>
</div>
<div style="padding: 8px 20px 16px;">
<div class="toggle-setting-row">
<div class="toggle-setting-text">
<div class="toggle-setting-label">Operation error spike</div>
<div class="toggle-setting-desc">Alert when error rate exceeds 5% over a 5-minute window.</div>
</div>
<div class="toggle-sm on" onclick="this.classList.toggle('on')"></div>
</div>
<div class="toggle-setting-row">
<div class="toggle-setting-text">
<div class="toggle-setting-label">Upstream latency degradation</div>
<div class="toggle-setting-desc">Alert when p99 latency exceeds the baseline by 3×.</div>
</div>
<div class="toggle-sm on" onclick="this.classList.toggle('on')"></div>
</div>
<div class="toggle-setting-row">
<div class="toggle-setting-text">
<div class="toggle-setting-label">Quota approaching limit</div>
<div class="toggle-setting-desc">Email when any operation reaches 80% of its monthly quota.</div>
</div>
<div class="toggle-sm on" onclick="this.classList.toggle('on')"></div>
</div>
<div class="toggle-setting-row">
<div class="toggle-setting-text">
<div class="toggle-setting-label">New team member joined</div>
<div class="toggle-setting-desc">Notify workspace admins when someone accepts an invite.</div>
</div>
<div class="toggle-sm" onclick="this.classList.toggle('on')"></div>
</div>
<div class="toggle-setting-row">
<div class="toggle-setting-text">
<div class="toggle-setting-label">Weekly usage digest</div>
<div class="toggle-setting-desc">Email summary of invocations, errors and latency each Monday.</div>
</div>
<div class="toggle-sm on" onclick="this.classList.toggle('on')"></div>
</div>
</div>
</div>
</div>
<!-- ■ DANGER ZONE — moved to workspace-setup.html ■ -->
<div id="section-danger" style="display:none;">
<div class="section-card" style="border-color: rgba(248,81,73,0.25);">
<div class="section-card-header" style="border-bottom-color: rgba(248,81,73,0.15);">
<div class="section-card-title" style="color: var(--red);">Danger zone</div>
</div>
<div>
<div class="danger-zone-action">
<div class="danger-zone-text">
<div class="danger-zone-title">Rename workspace</div>
<div class="danger-zone-desc">Changes the workspace slug used in all API endpoints. Existing API keys will continue to work. All existing tool call references will break.</div>
</div>
<button class="btn-danger" type="button">Rename</button>
</div>
<div class="danger-zone-action">
<div class="danger-zone-text">
<div class="danger-zone-title">Export all data</div>
<div class="danger-zone-desc">Download a ZIP archive of all operation configs, schemas, mappings and invocation history as JSON. May take a few minutes.</div>
</div>
<button class="btn-danger" type="button">Export</button>
</div>
<div class="danger-zone-action">
<div class="danger-zone-text">
<div class="danger-zone-title">Delete workspace</div>
<div class="danger-zone-desc">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" type="button" onclick="if(confirm('Delete workspace? This cannot be undone.')) { localStorage.clear(); window.location.href = 'login.html'; }">Delete workspace</button>
</div>
</div>
</div>
</div>
</div><!-- /settings-content -->
</div><!-- /settings-layout -->
</div><!-- /page -->
<script src="js/nav.js"></script>
<script src="js/settings.js"></script>
</body>
</html>
+228
View File
@@ -0,0 +1,228 @@
<!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">
<script>try{if(!localStorage.getItem('crank_user'))window.location.replace('login.html');}catch(e){}</script>
<script src="js/config.js"></script>
<script src="js/i18n.js"></script>
<script src="js/workspace.js"></script>
</head>
<body>
<nav class="navbar">
<a href="index.html" 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" style="display:none">
<div class="ws-dropdown-label">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.html?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>
Create workspace
</button>
</div>
</div>
</div>
<div class="nav-links">
<a href="index.html" class="nav-link" data-i18n="nav.operations">Operations</a>
<a href="html/agents.html" class="nav-link" data-i18n="nav.agents">Agents</a>
<a href="html/api-keys.html" class="nav-link" data-i18n="nav.apikeys">API Keys</a>
<a href="html/logs.html" class="nav-link" data-i18n="nav.logs">Logs</a>
<a href="html/usage.html" class="nav-link active" data-i18n="nav.usage">Usage</a>
</div>
<div class="nav-right">
<button class="nav-hamburger" aria-label="Menu"><span></span><span></span><span></span></button>
<div class="nav-kbd">
<svg width="12" height="12"><use href="icons/general/grid.svg#icon"/></svg>
<span>⌘K</span>
</div>
<button class="nav-icon-btn" 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" title="Account">AT</div>
<div class="user-dropdown">
<div class="user-dropdown-header">
<div class="user-dropdown-name">Operator</div>
<div class="user-dropdown-role">Admin · acme-workspace</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">
<a href="index.html" class="mobile-nav-link" data-i18n="nav.operations">Operations</a>
<a href="html/agents.html" class="mobile-nav-link" data-i18n="nav.agents">Agents</a>
<a href="html/api-keys.html" class="mobile-nav-link" data-i18n="nav.apikeys">API Keys</a>
<a href="html/logs.html" class="mobile-nav-link" data-i18n="nav.logs">Logs</a>
<a href="html/usage.html" 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">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>Last 7 days</option>
<option value="30d">Last 30 days</option>
<option value="90d">Last 90 days</option>
<option value="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>
Export CSV
</button>
</div>
</div>
<!-- Stat cards -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">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">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">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">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">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>Success</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>Error</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">By operation</div>
<div class="section-card-subtitle">Breakdown for last 7 days</div>
</div>
</div>
<table class="data-table">
<thead>
<tr>
<th>Operation</th>
<th>Protocol</th>
<th style="text-align:right;">Calls</th>
<th style="text-align:right;">Errors</th>
<th style="text-align:right;">Error rate</th>
<th>Latency (p50 / p95 / p99)</th>
<th style="text-align:right;">Quota</th>
</tr>
</thead>
<tbody id="usage-tbody">
</tbody>
</table>
</div>
</div>
<!-- Template: chart bar column -->
<template id="tmpl-chart-bar">
<div class="chart-col" style="flex:1;display:flex;flex-direction:column;align-items:center;gap:2px;justify-content:flex-end;position:relative;">
<div class="chart-bar error" style="width:100%;"></div>
<div class="chart-bar success" style="width:100%;"></div>
<div class="chart-col-label" style="position:absolute;bottom:-22px;font-size:10.5px;color:var(--text-muted);text-align:center;width:100%;"></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="js/nav.js"></script>
<script src="js/usage.js"></script>
</body>
</html>
+238
View File
@@ -0,0 +1,238 @@
<!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>try{if(!localStorage.getItem('crank_user'))window.location.replace('../login.html');}catch(e){}</script>
<script src="../../js/config.js"></script>
<script src="../../js/i18n.js"></script>
<script src="../../js/workspace.js"></script>
</head>
<body>
<!-- ═══════════════════ NAVBAR ═══════════════════ -->
<nav class="navbar">
<a href="../../index.html" 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" style="display:none">
<div class="ws-dropdown-label">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.html?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>
Create workspace
</button>
</div>
</div>
</div>
<div class="nav-links">
<a href="../../index.html" class="nav-link active" data-i18n="nav.operations">Operations</a>
<a href="../agents.html" class="nav-link" data-i18n="nav.agents">Agents</a>
<a href="../api-keys.html" class="nav-link" data-i18n="nav.apikeys">API Keys</a>
<a href="../logs.html" class="nav-link" data-i18n="nav.logs">Logs</a>
<a href="../usage.html" class="nav-link" data-i18n="nav.usage">Usage</a>
</div>
<div class="nav-right">
<button class="nav-hamburger" aria-label="Menu"><span></span><span></span><span></span></button>
<div class="nav-kbd">
<svg width="12" height="12"><use href="../../icons/general/grid.svg#icon"/></svg>
<span>⌘K</span>
</div>
<button class="nav-icon-btn" 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" title="Account">AT</div>
<div class="user-dropdown">
<div class="user-dropdown-header">
<div class="user-dropdown-name">Operator</div>
<div class="user-dropdown-role">Admin · acme-workspace</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">
<a href="../../index.html" class="mobile-nav-link active" data-i18n="nav.operations">Operations</a>
<a href="../agents.html" class="mobile-nav-link" data-i18n="nav.agents">Agents</a>
<a href="../api-keys.html" class="mobile-nav-link" data-i18n="nav.apikeys">API Keys</a>
<a href="../logs.html" class="mobile-nav-link" data-i18n="nav.logs">Logs</a>
<a href="../usage.html" 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" 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>
Operations
</button>
<div class="progress-divider"></div>
<span class="progress-label">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" 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">
Create <span>operation</span>
</div>
</div>
<div class="step-sidebar-body">
<div class="step-sidebar-section-label">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">Step 1</div>
<div class="step-name">Protocol</div>
<div class="step-status-text">In progress</div>
</div>
</div>
<div class="step-item pending">
<div class="step-indicator">2</div>
<div class="step-content">
<div class="step-number">Step 2</div>
<div class="step-name">Upstream target</div>
<div class="step-status-text">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">Step 3</div>
<div class="step-name" id="sidebar-step-3-name">Request config</div>
<div class="step-status-text">Not started</div>
</div>
</div>
<div class="step-item pending">
<div class="step-indicator">4</div>
<div class="step-content">
<div class="step-number">Step 4</div>
<div class="step-name">Tool config</div>
<div class="step-status-text">Not started</div>
</div>
</div>
<div class="step-item pending">
<div class="step-indicator">5</div>
<div class="step-content">
<div class="step-number">Step 5</div>
<div class="step-name">Mapping</div>
<div class="step-status-text">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>
Need help?
</div>
<div class="sidebar-help-text">
Unsure which protocol to choose? Read the
<a href="#" class="sidebar-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 style="opacity: 0.35; cursor: not-allowed;">
<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>
Back
</button>
<button class="btn-save-draft" type="button">Save draft</button>
<div class="action-bar-spacer"></div>
<span class="step-counter">Step <strong>1</strong> of 5</span>
<button id="btn-continue" class="btn btn-continue" type="button">
Continue
<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 -->
<script src="../../js/wizard.js"></script>
<script src="../../js/nav.js"></script>
</body>
</html>
+95
View File
@@ -0,0 +1,95 @@
<!-- ════════════ 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>
Step 1 of 5
</div>
<h1 class="step-panel-title">Choose a protocol</h1>
<p class="step-panel-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">
<div class="protocol-card rest selected" 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">REST / HTTP</div>
<div class="protocol-card-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 class="protocol-card graphql" role="radio" aria-checked="false" 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="#bc8cff" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2l9.5 5.5v11L12 22l-9.5-5.5v-11L12 2z"/>
<circle cx="12" cy="2" r="1.5" fill="#bc8cff"/>
<circle cx="21.5" cy="7.5" r="1.5" fill="#bc8cff"/>
<circle cx="21.5" cy="16.5" r="1.5" fill="#bc8cff"/>
<circle cx="12" cy="22" r="1.5" fill="#bc8cff"/>
<circle cx="2.5" cy="16.5" r="1.5" fill="#bc8cff"/>
<circle cx="2.5" cy="7.5" r="1.5" fill="#bc8cff"/>
</svg>
</div>
<div class="protocol-card-name">GraphQL</div>
<div class="protocol-card-tagline">Query and mutate a GraphQL schema. Supports queries, mutations, and subscriptions.</div>
<div class="protocol-card-tags">
<span class="protocol-tag">Query</span>
<span class="protocol-tag">Mutation</span>
</div>
</div>
<div class="protocol-card grpc" role="radio" aria-checked="false" 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="#0d9488" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="3" width="8" height="18" rx="2"/>
<rect x="14" y="3" width="8" height="18" rx="2"/>
<path d="M10 8h4M10 12h4M10 16h4"/>
</svg>
</div>
<div class="protocol-card-name">gRPC</div>
<div class="protocol-card-tagline">High-performance binary RPC over HTTP/2. Ideal for low-latency internal services.</div>
<div class="protocol-card-tags">
<span class="protocol-tag">Unary</span>
<span class="protocol-tag">Protobuf</span>
</div>
</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">You can change this later</div>
<div class="info-callout-text">
Switching protocol after configuring subsequent steps will clear schema and mapping data. Save your operation as a draft before experimenting. See <a href="#">protocol migration guide</a> for details.
</div>
</div>
</div>
</div><!-- /step-pane-1 -->
+157
View File
@@ -0,0 +1,157 @@
<!-- ════════════ 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>
Step 2 of 5
</div>
<h1 class="step-panel-title">Select upstream &amp; endpoint</h1>
<p class="step-panel-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">Upstream</div>
<div class="config-card-subtitle">Shared host, base URL, and authentication</div>
</div>
<span class="config-card-optional" style="color: var(--error, #f87171);">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">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" style="display:none;">
<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" 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" style="display:none;">
<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="openUpstreamDropdown()">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>Register new upstream</span>
</div>
<!-- Register new upstream form (kept as-is per design) -->
<div class="upstream-new-form" id="upstream-new-form" style="display:none;">
<div class="upstream-new-form-inner">
<div class="form-row" style="grid-template-columns: 1fr 2fr;">
<div class="form-group">
<label class="form-label">Name <span class="form-label-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">Unique identifier for this upstream.</div>
</div>
<div class="form-group">
<label class="form-label">Base URL <span class="form-label-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">Root URL — no trailing slash. Supports <code style="font-family:monospace;font-size:11px;color:var(--accent);">${secrets.*}</code>.</div>
</div>
</div>
<div class="form-group">
<label class="form-label">Auth headers</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 / auth-headers</span>
</div>
<textarea class="form-textarea code-textarea" rows="7">{
"Authorization": "${secrets.API_KEY}"
}</textarea>
</div>
<div class="form-hint">These headers are sent on every request to this upstream. Use secrets for credentials.</div>
</div>
<div style="display:flex; gap:8px; margin-top:4px;">
<button class="btn-primary-sm" onclick="saveNewUpstream(event)">Save upstream</button>
<button class="btn-ghost-sm" onclick="cancelNewUpstream(event)">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">Endpoint</div>
<div class="config-card-subtitle">Path and HTTP method for this specific operation</div>
</div>
<span class="config-card-optional" style="color: var(--error, #f87171);">Required</span>
</div>
<div class="config-card-body" style="gap: 16px;">
<div class="form-group">
<label class="form-label">
Path template
<span class="form-label-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">Appended to the upstream base URL. Use <code style="font-family:monospace;font-size:11px;color:var(--accent);">{param}</code> 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>
+92
View File
@@ -0,0 +1,92 @@
<div id="step-panel-3-graphql" class="step-pane">
<div class="step-panel-header">
<div class="step-panel-eyebrow">
<div class="step-panel-eyebrow-line"></div>
Step 3 of 5
</div>
<h1 class="step-panel-title">GraphQL operation</h1>
<p class="step-panel-subtitle">Define the GraphQL operation this tool will execute. All requests are sent as HTTP POST to the endpoint configured in the previous step. Variables are populated from MCP tool arguments at runtime.</p>
</div>
<!-- Operation type -->
<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="M5 8l2 2 4-4"/>
</svg>
</div>
<div>
<div class="config-card-title">Operation type</div>
<div class="config-card-subtitle">GraphQL operation kind — subscriptions are not supported</div>
</div>
<span class="config-card-optional" style="color:var(--error,#f87171);">Required</span>
</div>
<div class="config-card-body">
<div class="gql-type-grid">
<button class="gql-type-card active" data-gql-type="query" onclick="selectGqlType(this)">
<div class="gql-type-keyword">query</div>
<div class="gql-type-desc">Fetch data without side-effects</div>
</button>
<button class="gql-type-card" data-gql-type="mutation" onclick="selectGqlType(this)">
<div class="gql-type-keyword">mutation</div>
<div class="gql-type-desc">Create, update or delete data</div>
</button>
</div>
</div>
</div>
<!-- Query editor -->
<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">Query document</div>
<div class="config-card-subtitle">GraphQL document with typed variable declarations</div>
</div>
<span class="config-card-optional" style="color:var(--error,#f87171);">Required</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">graphql / query</span>
</div>
<textarea class="form-textarea code-textarea" rows="16" id="gql-query-editor" spellcheck="false">query GetContact($id: ID!) {
contact(id: $id) {
id
firstName
lastName
email
company {
id
name
}
createdAt
}
}</textarea>
</div>
</div>
</div>
<!-- Variables hint -->
<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">Variables are mapped in step 5</div>
<div class="info-callout-text">
Declare all variables in the query document using the <code style="font-family:monospace;font-size:11px;color:var(--accent);">$variable: Type</code> syntax. In step 5 you will map MCP input fields to these variables. Crank serialises the final <code style="font-family:monospace;font-size:11px;color:var(--accent);">{"query","variables"}</code> payload automatically.
</div>
</div>
</div>
</div><!-- /step-pane-3-graphql -->
+274
View File
@@ -0,0 +1,274 @@
<div id="step-panel-3-grpc" class="step-pane">
<div class="step-panel-header">
<div class="step-panel-eyebrow">
<div class="step-panel-eyebrow-line"></div>
Step 3 of 5
</div>
<h1 class="step-panel-title">RPC method</h1>
<p class="step-panel-subtitle">Choose how to describe the gRPC service contract. Upload a <code style="font-family:monospace;font-size:13px;color:var(--accent);">.proto</code> file, discover methods from a live server via reflection, or enter the route manually. Only unary RPCs are supported — streaming connections cannot be held open during MCP tool invocations.</p>
</div>
<!-- ── Source selector ── -->
<div class="grpc-source-tabs">
<button class="grpc-source-btn active" data-source="proto" onclick="selectGrpcSource('proto')">
<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="M9 2H4a1 1 0 00-1 1v10a1 1 0 001 1h8a1 1 0 001-1V6z"/><path d="M9 2v4h4"/></svg>
Proto file
</button>
<button class="grpc-source-btn" data-source="reflection" onclick="selectGrpcSource('reflection')">
<svg width="12" height="12" 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="2.5"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2M3.1 3.1l1.4 1.4M11.5 11.5l1.4 1.4M3.1 12.9l1.4-1.4M11.5 4.5l1.4-1.4"/></svg>
Server reflection
</button>
<button class="grpc-source-btn" data-source="manual" onclick="selectGrpcSource('manual')">
<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>
Manual
</button>
</div>
<!-- ══ Proto source ══ -->
<div id="grpc-src-proto">
<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="M9 2H4a1 1 0 00-1 1v10a1 1 0 001 1h8a1 1 0 001-1V6z"/><path d="M9 2v4h4"/></svg>
</div>
<div>
<div class="config-card-title">Proto definition</div>
<div class="config-card-subtitle">Upload or paste your .proto file</div>
</div>
<span class="config-card-optional" style="color:var(--error,#f87171);">Required</span>
</div>
<div class="config-card-body" style="gap: 12px; padding: 16px;">
<div class="proto-dropzone" id="proto-dropzone"
onclick="document.getElementById('proto-file-input').click()"
ondragover="event.preventDefault(); this.classList.add('drag-over')"
ondragleave="this.classList.remove('drag-over')"
ondrop="handleProtoDrop(event)">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--text-muted)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
</svg>
<div class="proto-dropzone-title">Drop <code style="font-family:monospace;font-size:12px;">.proto</code> file here</div>
<div class="proto-dropzone-sub">or click to browse</div>
<input type="file" id="proto-file-input" accept=".proto" style="display:none" onchange="handleProtoFile(event)">
</div>
<div class="proto-file-info" id="proto-file-info" style="display:none">
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="var(--accent)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M9 2H4a1 1 0 00-1 1v10a1 1 0 001 1h8a1 1 0 001-1V6z"/><path d="M9 2v4h4"/></svg>
<span class="proto-file-name" id="proto-file-name"></span>
<span class="proto-file-size" id="proto-file-size"></span>
<button class="proto-file-change" onclick="resetProto()">Change file</button>
</div>
<div style="display:flex; align-items:center; gap:10px;">
<div style="flex:1; height:1px; background:var(--border-subtle);"></div>
<button class="btn-ghost-sm" onclick="toggleProtoPaste()" id="proto-paste-btn">Paste content</button>
<div style="flex:1; height:1px; background:var(--border-subtle);"></div>
</div>
<div id="proto-paste-area" style="display:none">
<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">protobuf / definition</span>
</div>
<textarea class="form-textarea code-textarea" id="proto-paste-input" rows="14"
placeholder='syntax = "proto3";&#10;&#10;package acme;&#10;&#10;service ContactService {&#10; rpc GetContact (GetContactRequest) returns (Contact) {}&#10;}' spellcheck="false"></textarea>
</div>
<div style="display:flex; gap: 8px; margin-top: 8px;">
<button class="btn-primary-sm" onclick="parseProtoPasted()">Parse proto</button>
<button class="btn-ghost-sm" onclick="cancelProtoPaste()">Cancel</button>
</div>
</div>
</div>
</div>
<div id="proto-parsed-view" style="display:none">
<div class="info-callout" id="proto-streaming-notice" style="margin-bottom:20px;display:none">
<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">Streaming methods excluded</div>
<div class="info-callout-text"><span id="proto-streaming-count">0</span> streaming method(s) found and hidden — not compatible with MCP tool calls.</div>
</div>
</div>
<div class="config-card" style="margin-bottom: 20px;">
<div class="config-card-header">
<div><div class="config-card-title">Select method</div><div class="config-card-subtitle" id="proto-services-summary"></div></div>
<span class="config-card-optional" style="color:var(--error,#f87171);">Required</span>
</div>
<div class="config-card-body" style="padding: 0 16px 16px; gap: 0;">
<div id="proto-services-list"></div>
</div>
</div>
</div>
</div><!-- /grpc-src-proto -->
<!-- ══ Reflection source ══ -->
<div id="grpc-src-reflection" style="display:none">
<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="2.5"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2M3.1 3.1l1.4 1.4M11.5 11.5l1.4 1.4M3.1 12.9l1.4-1.4M11.5 4.5l1.4-1.4"/></svg>
</div>
<div>
<div class="config-card-title">Server reflection</div>
<div class="config-card-subtitle">Discover services from the upstream gRPC server at runtime</div>
</div>
</div>
<div class="config-card-body" style="gap: 14px; padding: 16px;">
<!-- Upstream info strip -->
<div class="grpc-reflect-upstream">
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-muted)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="3" width="6" height="10" rx="1"/><rect x="9" y="3" width="6" height="10" rx="1"/></svg>
<div class="grpc-reflect-upstream-info">
<span class="grpc-reflect-upstream-name" id="grpc-reflect-upstream-name">No upstream selected</span>
<span class="grpc-reflect-upstream-url" id="grpc-reflect-upstream-url">Select an upstream in step 2 first</span>
</div>
</div>
<!-- Idle -->
<div id="grpc-reflect-idle">
<button class="btn-primary-sm" onclick="startReflection()">
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" style="margin-right:4px"><circle cx="8" cy="8" r="2.5"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2"/></svg>
Discover methods
</button>
</div>
<!-- Loading -->
<div id="grpc-reflect-loading" style="display:none">
<div class="grpc-reflect-status">
<div class="grpc-reflect-spinner"></div>
<span id="grpc-reflect-status-text">Connecting…</span>
</div>
</div>
<!-- Error -->
<div id="grpc-reflect-error" style="display:none">
<div class="grpc-reflect-error-row">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="var(--error,#f87171)" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="8" r="7"/><path d="M8 5v3M8 10.5v.5"/></svg>
<span id="grpc-reflect-error-text">Reflection not available on this server.</span>
<button class="proto-file-change" onclick="startReflection()">Retry</button>
</div>
</div>
</div>
</div>
<div id="reflect-parsed-view" style="display:none">
<div class="info-callout" id="reflect-streaming-notice" style="margin-bottom:20px;display:none">
<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">Streaming methods excluded</div>
<div class="info-callout-text"><span id="reflect-streaming-count">0</span> streaming method(s) discovered and hidden.</div>
</div>
</div>
<div class="config-card" style="margin-bottom: 20px;">
<div class="config-card-header">
<div><div class="config-card-title">Select method</div><div class="config-card-subtitle" id="reflect-services-summary"></div></div>
<span class="config-card-optional" style="color:var(--error,#f87171);">Required</span>
</div>
<div class="config-card-body" style="padding: 0 16px 16px; gap: 0;">
<div id="reflect-services-list"></div>
</div>
</div>
</div>
</div><!-- /grpc-src-reflection -->
<!-- ══ Manual source ══ -->
<div id="grpc-src-manual" style="display:none">
<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">Method route</div>
<div class="config-card-subtitle">Full gRPC path to the unary method</div>
</div>
<span class="config-card-optional" style="color:var(--error,#f87171);">Required</span>
</div>
<div class="config-card-body" style="gap: 16px;">
<div class="form-group">
<label class="form-label">gRPC route <span class="form-label-required">required</span></label>
<input class="form-input input-mono" id="grpc-manual-route" type="text" placeholder="/package.ServiceName/MethodName" autocomplete="off" spellcheck="false" oninput="grpcManualRouteInput(this.value)">
<div class="form-hint">Format: <code style="font-family:monospace;font-size:11px;color:var(--accent);">/[package.]ServiceName/MethodName</code></div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Request type <span class="form-label-optional">(optional)</span></label>
<input class="form-input input-mono" id="grpc-manual-req-type" type="text" placeholder="GetContactRequest" autocomplete="off" spellcheck="false" oninput="grpcManualTypeInput()">
<div class="form-hint">Protobuf message name, for documentation.</div>
</div>
<div class="form-group">
<label class="form-label">Response type <span class="form-label-optional">(optional)</span></label>
<input class="form-input input-mono" id="grpc-manual-res-type" type="text" placeholder="Contact" autocomplete="off" spellcheck="false" oninput="grpcManualTypeInput()">
<div class="form-hint">Protobuf message name, for documentation.</div>
</div>
</div>
</div>
</div>
<div class="info-callout" 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">Input and output schemas are defined in step 4</div>
<div class="info-callout-text">In manual mode you only need the route. The MCP tool input/output schemas — which the LLM uses to call this tool — are configured in the next step regardless of Protobuf definitions.</div>
</div>
</div>
</div><!-- /grpc-src-manual -->
<!-- ══ Shared method detail (proto + reflection) ══ -->
<div id="grpc-method-detail" class="config-card" style="margin-bottom: 20px; display:none">
<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">Method signature</div>
<div class="config-card-subtitle" id="grpc-method-subtitle"></div>
</div>
</div>
<div class="config-card-body" style="gap: 16px; padding: 16px;">
<div class="proto-path-row">
<span class="proto-path-label">gRPC route</span>
<code class="proto-path-value" id="grpc-path-value"></code>
</div>
<div class="proto-sig-grid" id="grpc-detail-sig">
<div class="proto-sig-col">
<div class="proto-sig-col-header">
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M2 8h12M8 3l5 5-5 5"/></svg>
Request <code class="proto-type-badge" id="grpc-req-type"></code>
</div>
<div class="proto-field-list" id="grpc-req-fields"></div>
</div>
<div class="proto-sig-col">
<div class="proto-sig-col-header">
<svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M14 8H2M8 3l-5 5 5 5"/></svg>
Response <code class="proto-type-badge" id="grpc-res-type"></code>
</div>
<div class="proto-field-list" id="grpc-res-fields"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Template: proto service group -->
<template id="tmpl-proto-service-group">
<div class="proto-service-group">
<div class="proto-service-label">
<svg width="10" height="10" viewBox="0 0 16 16" fill="currentColor"><rect x="1" y="3" width="6" height="10" rx="1"/><rect x="9" y="3" width="6" height="10" rx="1"/></svg>
<span class="proto-service-name"></span>
</div>
<div class="proto-method-grid"></div>
</div>
</template>
<!-- Template: proto method card -->
<template id="tmpl-proto-method-card">
<button class="proto-method-card" onclick="selectRpcMethod(this)">
<span class="proto-method-name"></span>
<span class="proto-method-types">
<span class="proto-req-type"></span>
<svg width="10" height="10" viewBox="0 0 16 16" fill="currentColor"><path d="M3 8h10M9 4l4 4-4 4"/></svg>
<span class="proto-res-type"></span>
</span>
</button>
</template>
<!-- Template: proto field row -->
<template id="tmpl-proto-field-row">
<div class="proto-field-row">
<span class="proto-field-name"></span>
<span class="proto-field-type"></span>
</div>
</template>
+93
View File
@@ -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>
Step 3 of 5
</div>
<h1 class="step-panel-title">HTTP method &amp; format</h1>
<p class="step-panel-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">HTTP method</div>
<div class="config-card-subtitle">Verb sent to the upstream on every tool invocation</div>
</div>
<span class="config-card-optional" style="color:var(--error,#f87171);">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">Read</span>
</button>
<button class="method-card active" data-method="POST" onclick="selectMethod(this)">
<span class="method-name">POST</span>
<span class="method-desc">Create</span>
</button>
<button class="method-card" data-method="PUT" onclick="selectMethod(this)">
<span class="method-name">PUT</span>
<span class="method-desc">Replace</span>
</button>
<button class="method-card" data-method="PATCH" onclick="selectMethod(this)">
<span class="method-name">PATCH</span>
<span class="method-desc">Update</span>
</button>
<button class="method-card" data-method="DELETE" onclick="selectMethod(this)">
<span class="method-name">DELETE</span>
<span class="method-desc">Remove</span>
</button>
</div>
<div class="method-callout" id="method-callout-rest" style="display:none;"></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">Request format</div>
<div class="config-card-subtitle">Content negotiation and serialisation</div>
</div>
<span class="config-card-optional">Optional</span>
</div>
<div class="config-card-body" style="gap: 16px;">
<div class="form-row">
<div class="form-group">
<label class="form-label">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">How the request body is encoded when sent to the upstream.</div>
</div>
<div class="form-group">
<label class="form-label">Accept</label>
<select class="form-select">
<option selected>application/json</option>
<option>text/plain</option>
<option>*/*</option>
</select>
<div class="form-hint">Accepted response content types from the upstream server.</div>
</div>
</div>
</div>
</div>
</div><!-- /step-pane-3-rest -->
+162
View File
@@ -0,0 +1,162 @@
<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>
Step 4 of 5
</div>
<h1 class="step-panel-title">Tool config</h1>
<p class="step-panel-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">Tool identity</div>
<div class="config-card-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">
Tool name
<span class="form-label-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">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">
Display name
<span class="form-label-required">required</span>
</label>
<input id="tool-display-name" class="form-input" type="text" value="Create CRM Lead" autocomplete="off">
<div class="form-hint">Human-readable name shown in the console and audit log.</div>
</div>
<div class="form-group">
<label class="form-label">
Tool title
<span class="form-label-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">Short imperative sentence shown in the MCP tool manifest.</div>
</div>
</div>
<div class="form-group">
<label class="form-label">
Description
<span class="form-label-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">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">Writing effective descriptions</div>
<div class="info-callout-text">
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 <a href="#">description best practices →</a>
</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">Input schema</div>
<div class="config-card-subtitle">Parameters the LLM passes when calling this tool</div>
</div>
<span class="config-card-optional">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">Output schema</div>
<div class="config-card-subtitle">Shape of the data returned to the LLM after a successful call</div>
</div>
<span class="config-card-optional">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">Schemas are protocol-agnostic</div>
<div class="info-callout-text">
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 -->
+102
View File
@@ -0,0 +1,102 @@
<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>
Step 5 of 5
</div>
<h1 class="step-panel-title">Mapping and execution</h1>
<p class="step-panel-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 3.</p>
</div>
<div class="section-divider" style="margin-bottom: 16px;">
<span class="section-divider-label">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">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">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">Execution config</div>
<div class="config-card-subtitle">Timeouts, retry policy and auth profile reference</div>
</div>
<span class="config-card-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" 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">Operation will be saved as a draft</div>
<div class="info-callout-text">
Clicking "Create operation" saves the contract in draft status. You can run tests, inspect metadata and publish from the operation workspace. Drafts are never exposed to MCP clients.
</div>
</div>
</div>
</div><!-- /step-pane-5 -->
+327
View File
@@ -0,0 +1,327 @@
<!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>try{if(!localStorage.getItem('crank_user'))window.location.replace('login.html');}catch(e){}</script>
<script src="js/config.js"></script>
<script src="js/i18n.js"></script>
</head>
<body>
<div class="ws-setup-page">
<!-- Top bar -->
<div class="ws-setup-topbar">
<a href="index.html" 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>
Back
</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">Workspace settings</div>
<div class="ws-setup-subtitle" id="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">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">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">Workspace name <span class="form-label-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">Slug <span class="form-label-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">Only lowercase letters, numbers, and hyphens. Used in MCP endpoint URLs.</div>
</div>
<div class="form-group">
<label class="form-label">Description <span class="form-label-optional">(optional)</span></label>
<textarea class="form-textarea" id="ws-desc" rows="2" placeholder="What does this workspace do?"></textarea>
</div>
</div>
<!-- ── Default settings ── -->
<div class="ws-form-section">
<div class="ws-form-section-title">Default settings</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Default protocol</label>
<select class="form-input" id="ws-protocol">
<option value="rest" selected>REST</option>
<option value="graphql">GraphQL</option>
<option value="grpc">gRPC</option>
</select>
<div class="form-hint">Pre-selected in the operation wizard.</div>
</div>
<div class="form-group">
<label class="form-label">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">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" style="display:none; 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;">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>
Invite member
</button>
</div>
<!-- Invite form -->
<div id="invite-form" style="display:none; 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">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">Role</label>
<select class="form-input" id="invite-role">
<option value="admin">Admin</option>
<option value="developer" selected>Developer</option>
<option value="viewer">Viewer</option>
</select>
</div>
<button class="btn-primary" type="button" style="padding:7px 14px;font-size:13px;" onclick="sendInvite()">Send invite</button>
<button class="btn-ghost-sm" type="button" style="padding:7px 12px;" onclick="toggleInviteForm()">Cancel</button>
</div>
<div id="invite-success" style="display:none; margin-top:10px; font-size:13px; color: #3fb950;">✓ 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">Operator <span style="font-size:11px;font-weight:400;color:var(--text-muted);">(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">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>Admin</option>
<option value="developer">Developer</option>
<option value="viewer">Viewer</option>
</select>
<button class="member-remove-btn" onclick="removeMember(this, 'Blake Smith')" 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">Admin</option>
<option value="developer" selected>Developer</option>
<option value="viewer">Viewer</option>
</select>
<button class="member-remove-btn" onclick="removeMember(this, 'Carol Lane')" 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">Admin</option>
<option value="developer">Developer</option>
<option value="viewer" selected>Viewer</option>
</select>
<button class="member-remove-btn" onclick="removeMember(this, 'Dan Morgan')" 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" 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;">
Pending invites <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);">Developer</span>
<button class="member-remove-btn" onclick="revokeInvite(this)" 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;">Roles &amp; permissions</div>
<div class="role-def-row">
<span class="member-role-badge role-owner" style="min-width:80px;text-align:center;">Owner</span>
<div class="role-def-text">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;">Admin</span>
<div class="role-def-text">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;">Developer</span>
<div class="role-def-text">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;">Viewer</span>
<div class="role-def-text">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" style="display:none;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px;">
<div class="ws-form-section-title" style="margin-bottom:0;">Invite team members <span style="font-size:12px;font-weight:400;color:var(--text-muted);">(optional)</span></div>
<button class="btn-ghost-sm" type="button" onclick="addInviteRow()" style="font-size:12px;padding:4px 10px;">+ Add person</button>
</div>
<div class="form-hint" style="margin-bottom:14px;">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">Admin</option>
<option value="developer" selected>Developer</option>
<option value="viewer">Viewer</option>
</select>
<button class="invite-row-remove" onclick="removeInviteRow(this)" 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);">Cancel</a>
<button class="btn-primary" id="submit-btn" type="button" onclick="submitForm()" style="padding:9px 22px;font-size:13px;">Save changes</button>
</div>
<div class="ws-setup-footer-note" id="footer-note" style="display:none;">
You'll be the Owner of this workspace. You can invite additional members after creation.
</div>
<!-- ── Danger zone (edit mode only) ── -->
<div id="section-danger" style="display:none; 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);">Danger zone</div>
</div>
<div class="danger-zone-action">
<div class="danger-zone-text">
<div class="danger-zone-title">Export all data</div>
<div class="danger-zone-desc">Download a ZIP archive of all operation configs, schemas, mappings and invocation history as JSON.</div>
</div>
<button class="btn-danger" type="button">Export</button>
</div>
<div class="danger-zone-action">
<div class="danger-zone-text">
<div class="danger-zone-title">Delete workspace</div>
<div class="danger-zone-desc">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" type="button" onclick="if(confirm('Delete workspace? This cannot be undone.')) { localStorage.clear(); window.location.href = 'login.html'; }">Delete workspace</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="js/workspace.js"></script>
<script src="js/workspace-setup.js"></script>
</body>
</html>