Усилить безопасность веб-интерфейса
CI / Rust Checks (push) Successful in 5m14s
CI / UI Checks (push) Successful in 4s
CI / Deployment Manifests (push) Successful in 2s
CI / Frontend E2E (push) Successful in 2m58s
CI / Deploy (push) Successful in 1m44s

This commit is contained in:
2026-07-11 17:12:50 +03:00
parent 8318e4b560
commit 46892ee61c
26 changed files with 250 additions and 60 deletions
+97
View File
@@ -0,0 +1,97 @@
/* Local font assets are copied from pinned @fontsource packages during the UI build. */
@font-face {
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 400;
src: url('../fonts/inter-cyrillic-400-normal.woff2') format('woff2');
unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 400;
src: url('../fonts/inter-latin-400-normal.woff2') format('woff2');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 500;
src: url('../fonts/inter-cyrillic-500-normal.woff2') format('woff2');
unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 500;
src: url('../fonts/inter-latin-500-normal.woff2') format('woff2');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 600;
src: url('../fonts/inter-cyrillic-600-normal.woff2') format('woff2');
unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 600;
src: url('../fonts/inter-latin-600-normal.woff2') format('woff2');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 700;
src: url('../fonts/inter-cyrillic-700-normal.woff2') format('woff2');
unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 700;
src: url('../fonts/inter-latin-700-normal.woff2') format('woff2');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-display: swap;
font-weight: 400;
src: url('../fonts/jetbrains-mono-cyrillic-400-normal.woff2') format('woff2');
unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-display: swap;
font-weight: 400;
src: url('../fonts/jetbrains-mono-latin-400-normal.woff2') format('woff2');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-display: swap;
font-weight: 500;
src: url('../fonts/jetbrains-mono-cyrillic-500-normal.woff2') format('woff2');
unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-display: swap;
font-weight: 500;
src: url('../fonts/jetbrains-mono-latin-500-normal.woff2') format('woff2');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
+5
View File
@@ -37,7 +37,11 @@
color: var(--text-muted);
text-decoration: none;
padding: 6px 10px;
border: 0;
background: none;
border-radius: 6px;
font-family: inherit;
cursor: pointer;
transition: color 0.15s, background 0.15s;
}
.ws-setup-back:hover { color: var(--text-secondary); background: rgba(255,255,255,0.04); }
@@ -107,6 +111,7 @@
.ws-color-swatch {
width: 20px;
height: 20px;
padding: 0;
border-radius: 5px;
cursor: pointer;
border: 2px solid transparent;
+4 -4
View File
@@ -5,7 +5,7 @@
<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/fonts.css">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/pages.css">
@@ -49,12 +49,12 @@
<div class="user-dropdown-name">Crank</div>
<div class="user-dropdown-role" id="user-ws-role"></div>
</div>
<button class="user-dropdown-item" onclick="window.location.href='/settings'">
<a class="user-dropdown-item" href="/settings">
<svg width="13" height="13"><use href="icons/general/settings.svg#icon"/></svg>
<span data-i18n="nav.settings">Settings</span>
</button>
</a>
<div class="dropdown-divider"></div>
<button class="user-dropdown-item danger" onclick="window.CrankAuth.logout()">
<button class="user-dropdown-item danger" @click="window.CrankAuth.logout()">
<svg width="13" height="13"><use href="icons/general/logout.svg#icon"/></svg>
<span data-i18n="nav.logout">Log out</span>
</button>
+1 -1
View File
@@ -5,7 +5,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Crank — Agent Keys</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/fonts.css">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/pages.css">
+2 -2
View File
@@ -1,11 +1,11 @@
<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')">
<button class="lang-btn" data-lang="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')">
<button class="lang-btn" data-lang="ru">
<span class="lang-flag">🇷🇺</span>
<span data-i18n="settings.lang.ru">Русский</span>
</button>
+1 -1
View File
@@ -5,7 +5,7 @@
<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/fonts.css">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/login.css">
<script src="%CRANK_BUNDLE_LOGIN%"></script>
+1 -1
View File
@@ -5,7 +5,7 @@
<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/fonts.css">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/pages.css">
+1 -1
View File
@@ -5,7 +5,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Crank — Secrets</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/fonts.css">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/pages.css">
+3 -3
View File
@@ -5,7 +5,7 @@
<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/fonts.css">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/pages.css">
@@ -172,11 +172,11 @@
<label class="field-label" data-i18n="settings.lang.title">Language</label>
<div class="field-hint" data-i18n="settings.lang.subtitle">Interface display language</div>
<div class="lang-switcher">
<button class="lang-btn" data-lang="en" type="button" onclick="setLang('en')">
<button class="lang-btn" data-lang="en" type="button">
<span class="lang-flag">🇬🇧</span>
<span data-i18n="settings.lang.en">English</span>
</button>
<button class="lang-btn" data-lang="ru" type="button" onclick="setLang('ru')">
<button class="lang-btn" data-lang="ru" type="button">
<span class="lang-flag">🇷🇺</span>
<span data-i18n="settings.lang.ru">Русский</span>
</button>
+1 -1
View File
@@ -5,7 +5,7 @@
<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/fonts.css">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/pages.css">
+1 -1
View File
@@ -4,7 +4,7 @@
<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/fonts.css">
<link rel="stylesheet" href="../../css/variables.css">
<link rel="stylesheet" href="../../css/layout.css">
<link rel="stylesheet" href="../../css/wizard.css">
+9 -9
View File
@@ -30,7 +30,7 @@
<!-- 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-trigger" id="upstream-combobox-trigger" data-wizard-action="toggle-upstream">
<div class="upstream-combobox-value" id="upstream-combobox-value">
<span class="upstream-combobox-placeholder" data-i18n="wizard.step2.upstream_placeholder">Выберите API-хост…</span>
</div>
@@ -45,7 +45,7 @@
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="var(--text-muted)" stroke-width="1.8" stroke-linecap="round">
<circle cx="7" cy="7" r="5"/><path d="M12 12l2.5 2.5"/>
</svg>
<input class="upstream-search-input" id="upstream-search" type="text" data-i18n-ph="wizard.step2.search_placeholder" placeholder="Поиск по имени или URL…" oninput="filterUpstreams(this.value)" autocomplete="off" spellcheck="false">
<input class="upstream-search-input" id="upstream-search" type="text" data-i18n-ph="wizard.step2.search_placeholder" placeholder="Поиск по имени или URL…" autocomplete="off" spellcheck="false">
</div>
<div class="upstream-dropdown-list" id="upstream-dropdown-list">
<!-- populated by JS -->
@@ -58,11 +58,11 @@
<div class="upstream-preview-name" id="upstream-preview-name"></div>
<div class="upstream-preview-url" id="upstream-preview-url"></div>
<span class="upstream-auth-badge" id="upstream-preview-badge"></span>
<button class="upstream-preview-change" onclick="beginEditSelectedUpstream(event)" data-i18n="wizard.step2.change">Изменить</button>
<button class="upstream-preview-change" data-wizard-action="edit-upstream" data-i18n="wizard.step2.change">Изменить</button>
</div>
<!-- Register new upstream trigger row -->
<div class="upstream-new-trigger" id="upstream-new-trigger" onclick="startNewUpstream()">
<div class="upstream-new-trigger" id="upstream-new-trigger" data-wizard-action="new-upstream">
<div class="upstream-new-trigger-radio" id="upstream-new-trigger-radio">
<div class="upstream-new-trigger-dot"></div>
</div>
@@ -89,7 +89,7 @@
</div>
<div class="form-group">
<label class="form-label" data-i18n="wizard.step2.auth_selector">Авторизация API-хоста</label>
<select class="form-select" id="new-upstream-auth-mode" data-testid="wizard-auth-mode-select" onchange="updateUpstreamAuthUi()">
<select class="form-select" id="new-upstream-auth-mode" data-testid="wizard-auth-mode-select">
<option value="none" data-i18n="wizard.step2.auth_mode.none">Без авторизации</option>
<option value="existing" data-i18n="wizard.step2.auth_mode.existing">Использовать существующий профиль авторизации</option>
<option value="create" data-i18n="wizard.step2.auth_mode.create">Создать профиль авторизации сейчас</option>
@@ -118,7 +118,7 @@
<div class="form-row" style="grid-template-columns: 1fr 1fr;">
<div class="form-group">
<label class="form-label"><span data-i18n="wizard.step2.profile_kind">Тип авторизации</span> <span class="form-label-required" data-i18n="workspace_setup.required">обязательно</span></label>
<select class="form-select" id="new-auth-profile-kind" data-testid="wizard-auth-profile-kind-select" onchange="updateAuthProfileCreateUi()">
<select class="form-select" id="new-auth-profile-kind" data-testid="wizard-auth-profile-kind-select">
<option value="bearer" data-i18n="wizard.step2.auth_kind.bearer">Bearer-токен</option>
<option value="basic" data-i18n="wizard.step2.auth_kind.basic">Логин и пароль</option>
<option value="api_key_header" data-i18n="wizard.step2.auth_kind.api_key_header">API-ключ в заголовке</option>
@@ -149,7 +149,7 @@
<select class="form-select" id="new-auth-profile-secret-id" data-testid="wizard-auth-secret-select"></select>
</div>
<div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
<button class="btn-ghost-sm" data-testid="wizard-open-quick-secret" onclick="openQuickSecretModal(event)" data-i18n="wizard.step2.quick_secret">Быстро создать секрет</button>
<button class="btn-ghost-sm" data-testid="wizard-open-quick-secret" data-wizard-action="quick-secret" data-i18n="wizard.step2.quick_secret">Быстро создать секрет</button>
<a class="btn-ghost-sm" href="/secrets" target="_blank" rel="noopener" data-i18n="wizard.step2.manage_secrets">Открыть страницу секретов</a>
</div>
</div>
@@ -168,8 +168,8 @@
<div class="form-hint" data-i18n="wizard.step2.static_headers_hint">Необязательные заголовки без секретных значений. Токены, пароли и ключи храните через профиль авторизации.</div>
</div>
<div style="display:flex; gap:8px; margin-top:4px;">
<button class="btn-primary-sm" onclick="saveNewUpstream(event)" data-i18n="wizard.step2.save_upstream">Сохранить API-хост</button>
<button class="btn-ghost-sm" onclick="cancelNewUpstream(event)" data-i18n="btn.cancel">Отмена</button>
<button class="btn-primary-sm" data-wizard-action="save-upstream" data-i18n="wizard.step2.save_upstream">Сохранить API-хост</button>
<button class="btn-ghost-sm" data-wizard-action="cancel-upstream" data-i18n="btn.cancel">Отмена</button>
</div>
</div>
</div>
+5 -5
View File
@@ -25,23 +25,23 @@
</div>
<div class="config-card-body">
<div class="method-grid">
<button class="method-card" data-method="GET" onclick="selectMethod(this)">
<button class="method-card" data-method="GET">
<span class="method-name">GET</span>
<span class="method-desc" data-i18n="wizard.step3.rest.read">Чтение</span>
</button>
<button class="method-card active" data-method="POST" onclick="selectMethod(this)">
<button class="method-card active" data-method="POST">
<span class="method-name">POST</span>
<span class="method-desc" data-i18n="wizard.step3.rest.create">Создание</span>
</button>
<button class="method-card" data-method="PUT" onclick="selectMethod(this)">
<button class="method-card" data-method="PUT">
<span class="method-name">PUT</span>
<span class="method-desc" data-i18n="wizard.step3.rest.replace">Замена</span>
</button>
<button class="method-card" data-method="PATCH" onclick="selectMethod(this)">
<button class="method-card" data-method="PATCH">
<span class="method-name">PATCH</span>
<span class="method-desc" data-i18n="wizard.step3.rest.update">Обновление</span>
</button>
<button class="method-card" data-method="DELETE" onclick="selectMethod(this)">
<button class="method-card" data-method="DELETE">
<span class="method-name">DELETE</span>
<span class="method-desc" data-i18n="wizard.step3.rest.remove">Удаление</span>
</button>
+14 -14
View File
@@ -5,7 +5,7 @@
<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/fonts.css">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/pages.css">
@@ -24,10 +24,10 @@
</div>
Crank
</a>
<a href="javascript:history.back()" class="ws-setup-back">
<button type="button" class="ws-setup-back" data-history-back>
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10 4l-4 4 4 4"/></svg>
<span data-i18n="workspace_setup.back">Back</span>
</a>
</button>
</div>
<!-- Body -->
@@ -48,27 +48,27 @@
<div>
<div class="ws-avatar-hint" data-i18n="workspace_setup.identity.avatar_hint">Avatar is derived from your workspace name.</div>
<div class="ws-color-swatches">
<div class="ws-color-swatch active" style="background:#0d9488;" data-color="#0d9488" onclick="pickColor(this)" title="Teal"></div>
<div class="ws-color-swatch" style="background:#7c3aed;" data-color="#7c3aed" onclick="pickColor(this)" title="Purple"></div>
<div class="ws-color-swatch" style="background:#0891b2;" data-color="#0891b2" onclick="pickColor(this)" title="Cyan"></div>
<div class="ws-color-swatch" style="background:#d29922;" data-color="#d29922" onclick="pickColor(this)" title="Amber"></div>
<div class="ws-color-swatch" style="background:#1a7f37;" data-color="#1a7f37" onclick="pickColor(this)" title="Green"></div>
<div class="ws-color-swatch" style="background:#cf222e;" data-color="#cf222e" onclick="pickColor(this)" title="Red"></div>
<div class="ws-color-swatch" style="background:#5e6ad2;" data-color="#5e6ad2" onclick="pickColor(this)" title="Indigo"></div>
<button class="ws-color-swatch active" style="background:#0d9488;" data-color="#0d9488" type="button" title="Teal"></button>
<button class="ws-color-swatch" style="background:#7c3aed;" data-color="#7c3aed" type="button" title="Purple"></button>
<button class="ws-color-swatch" style="background:#0891b2;" data-color="#0891b2" type="button" title="Cyan"></button>
<button class="ws-color-swatch" style="background:#d29922;" data-color="#d29922" type="button" title="Amber"></button>
<button class="ws-color-swatch" style="background:#1a7f37;" data-color="#1a7f37" type="button" title="Green"></button>
<button class="ws-color-swatch" style="background:#cf222e;" data-color="#cf222e" type="button" title="Red"></button>
<button class="ws-color-swatch" style="background:#5e6ad2;" data-color="#5e6ad2" type="button" title="Indigo"></button>
</div>
</div>
</div>
<div class="form-group" style="margin-bottom:14px;">
<label class="form-label"><span data-i18n="workspace_setup.name">Workspace name</span> <span class="form-label-required" data-i18n="workspace_setup.required">required</span></label>
<input class="form-input" id="ws-name" type="text" placeholder="Acme Inc" autocomplete="off" oninput="onWsNameInput(this.value)">
<input class="form-input" id="ws-name" type="text" placeholder="Acme Inc" autocomplete="off">
</div>
<div class="form-group" style="margin-bottom:14px;">
<label class="form-label"><span data-i18n="workspace_setup.slug">Slug</span> <span class="form-label-required" data-i18n="workspace_setup.required">required</span></label>
<div style="position:relative;">
<span style="position:absolute;left:12px;top:50%;transform:translateY(-50%);font-size:13px;color:var(--text-muted);font-family:monospace;pointer-events:none;">mcp.crank.io/</span>
<input class="form-input input-mono" id="ws-slug" type="text" placeholder="acme-inc" autocomplete="off" style="padding-left: 112px;" oninput="onWsSlugInput(this.value)">
<input class="form-input input-mono" id="ws-slug" type="text" placeholder="acme-inc" autocomplete="off" style="padding-left: 112px;">
</div>
<div class="form-hint" data-i18n="workspace_setup.slug_hint">Only lowercase letters, numbers, and hyphens. Used in MCP endpoint URLs.</div>
</div>
@@ -81,8 +81,8 @@
<!-- ── Actions ── -->
<div class="ws-setup-actions">
<a href="javascript:history.back()" class="btn-ghost-sm" style="padding:9px 18px;font-size:13px;text-decoration:none;color:var(--text-secondary);" data-i18n="btn.cancel">Cancel</a>
<button class="btn-primary ws-submit-btn" id="submit-btn" type="button" onclick="submitForm()" data-i18n="workspace_setup.actions.save">Save changes</button>
<button type="button" class="btn-ghost-sm" data-history-back style="padding:9px 18px;font-size:13px;text-decoration:none;color:var(--text-secondary);" data-i18n="btn.cancel">Cancel</button>
<button class="btn-primary ws-submit-btn" id="submit-btn" type="button" data-i18n="workspace_setup.actions.save">Save changes</button>
</div>
<div class="ws-setup-footer-note" id="footer-note" hidden>
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Crank — Operations</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/fonts.css">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/catalog.css">
-8
View File
@@ -124,14 +124,6 @@
return encoded ? ('?' + encoded) : '';
}
function postBytes(path, bytes, fileName) {
return request(API_BASE + path, {
method: 'POST',
headers: headers(fileName ? { 'X-File-Name': fileName } : {}),
body: bytes,
});
}
window.CrankApi = {
login: function(payload) {
return request(AUTH_BASE + '/login', {
+3
View File
@@ -330,6 +330,9 @@ async function loadWorkspaceSettings() {
}
async function initSettingsPage() {
document.querySelectorAll('.lang-btn[data-lang]').forEach(function(button) {
button.addEventListener('click', function() { setLang(button.dataset.lang); });
});
bindSectionNavigation();
await loadProfile();
await loadCapabilities();
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+31
View File
@@ -83,6 +83,7 @@ async function initWizardPage() {
await loadProtocolCapabilities();
await loadWizardPanels([1, 2, 3, 4, 5]);
bindWizardPanelActions();
if (window.CrankOverlay && typeof window.CrankOverlay.render === 'function') {
await window.CrankOverlay.render(document, {
workspace: workspace,
@@ -248,6 +249,36 @@ function selectMethod(btn) {
}
}
function bindWizardPanelActions() {
var upstreamSearch = document.getElementById('upstream-search');
var authMode = document.getElementById('new-upstream-auth-mode');
var authKind = document.getElementById('new-auth-profile-kind');
if (upstreamSearch) {
upstreamSearch.addEventListener('input', function(event) {
filterUpstreams(event.target.value);
});
}
if (authMode) authMode.addEventListener('change', updateUpstreamAuthUi);
if (authKind) authKind.addEventListener('change', updateAuthProfileCreateUi);
document.querySelectorAll('.method-card[data-method]').forEach(function(button) {
button.addEventListener('click', function() { selectMethod(button); });
});
document.querySelectorAll('[data-wizard-action]').forEach(function(element) {
element.addEventListener('click', function(event) {
var action = element.dataset.wizardAction;
if (action === 'toggle-upstream') toggleUpstreamDropdown(event);
if (action === 'edit-upstream') beginEditSelectedUpstream(event);
if (action === 'new-upstream') startNewUpstream();
if (action === 'quick-secret') openQuickSecretModal(event);
if (action === 'save-upstream') void saveNewUpstream(event);
if (action === 'cancel-upstream') cancelNewUpstream(event);
});
});
}
function escapeHtml(str) {
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
+11
View File
@@ -207,6 +207,17 @@ async function exportWorkspaceSnapshot() {
async function initPage() {
updatePageMode();
document.querySelectorAll('.ws-color-swatch').forEach(function(swatch) {
swatch.addEventListener('click', function() { pickColor(swatch); });
});
document.querySelectorAll('[data-history-back]').forEach(function(button) {
button.addEventListener('click', function() { window.history.back(); });
});
var elements = formElements();
elements.name.addEventListener('input', function(event) { onWsNameInput(event.target.value); });
elements.slug.addEventListener('input', function(event) { onWsSlugInput(event.target.value); });
elements.submit.addEventListener('click', submitForm);
var exportButton = document.getElementById('export-workspace-btn');
if (exportButton) {
exportButton.addEventListener('click', exportWorkspaceSnapshot);
+7
View File
@@ -6,6 +6,13 @@ server {
root /usr/share/nginx/html;
index index.html;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data: blob:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'" always;
location = / {
try_files /index.html =404;
}
+20
View File
@@ -6,6 +6,8 @@
"": {
"name": "crank-ui",
"dependencies": {
"@fontsource/inter": "5.2.8",
"@fontsource/jetbrains-mono": "5.2.8",
"alpinejs": "3.15.12",
"js-yaml": "5.2.1"
},
@@ -456,6 +458,24 @@
"node": ">=18"
}
},
"node_modules/@fontsource/inter": {
"version": "5.2.8",
"resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz",
"integrity": "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/jetbrains-mono": {
"version": "5.2.8",
"resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz",
"integrity": "sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@playwright/test": {
"version": "1.61.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
+2
View File
@@ -8,6 +8,8 @@
"e2e:headed": "playwright test --headed"
},
"dependencies": {
"@fontsource/inter": "5.2.8",
"@fontsource/jetbrains-mono": "5.2.8",
"alpinejs": "3.15.12",
"js-yaml": "5.2.1"
},
+19
View File
@@ -10,6 +10,10 @@ const BRAND_IMAGE_PATHS = [
path.join(ROOT_DIR, 'crank-community.png'),
path.resolve(ROOT_DIR, '..', '..', 'crank-community.png'),
];
const FONT_FILES = [
['@fontsource/inter', 'inter', ['400', '500', '600', '700']],
['@fontsource/jetbrains-mono', 'jetbrains-mono', ['400', '500']],
];
const BUNDLES = {
'protected-core': {
@@ -136,6 +140,20 @@ function copyDirectory(source, destination) {
}
}
function copyFonts() {
FONT_FILES.forEach(function([packageName, family, weights]) {
weights.forEach(function(weight) {
['latin', 'cyrillic'].forEach(function(subset) {
var fileName = `${family}-${subset}-${weight}-normal.woff2`;
copyFile(
path.join(ROOT_DIR, 'node_modules', packageName, 'files', fileName),
path.join(DIST_DIR, 'fonts', fileName)
);
});
});
});
}
function readSource(relativePath) {
return fs.readFileSync(sourcePath(relativePath), 'utf8');
}
@@ -191,6 +209,7 @@ async function main() {
}
copyDirectory(path.join(ROOT_DIR, 'css'), path.join(DIST_DIR, 'css'));
copyFonts();
copyDirectory(path.join(ROOT_DIR, 'data'), path.join(DIST_DIR, 'data'));
ensureDirectory(path.join(DIST_DIR, 'html', 'wizard'));
[
+11 -1
View File
@@ -119,8 +119,18 @@ function proxyRequest(request, response) {
},
},
(proxyResponse) => {
if (response.destroyed || response.writableEnded) {
proxyResponse.resume();
return;
}
response.writeHead(proxyResponse.statusCode || 502, proxyResponse.headers);
pipeline(proxyResponse, response, () => {});
proxyResponse.on('error', function() {
if (!response.destroyed) response.destroy();
});
response.on('close', function() {
if (!proxyResponse.destroyed) proxyResponse.destroy();
});
proxyResponse.pipe(response);
},
);