From 7070231c3e9e1defb404c0b542a550dedb5d728e Mon Sep 17 00:00:00 2001 From: "a.tolmachev" Date: Mon, 30 Mar 2026 00:15:02 +0300 Subject: [PATCH] feat: migrate alpine ui into app shell --- TASKS.md | 14 +- apps/ui/Dockerfile | 5 + apps/ui/css/catalog.css | 579 +++++++ apps/ui/css/layout.css | 231 +++ apps/ui/css/login.css | 192 +++ apps/ui/css/logs.css | 125 ++ apps/ui/css/pages.css | 1458 ++++++++++++++++ apps/ui/css/settings.css | 144 ++ apps/ui/css/usage.css | 122 ++ apps/ui/css/variables.css | 64 + apps/ui/css/wizard.css | 1884 +++++++++++++++++++++ apps/ui/css/workspace-setup.css | 223 +++ apps/ui/data/agents.json | 58 + apps/ui/data/keys.json | 15 + apps/ui/data/operations.json | 134 ++ apps/ui/html/agents.html | 380 +++++ apps/ui/html/api-keys.html | 281 +++ apps/ui/html/fragments/lang-switcher.html | 13 + apps/ui/html/login.html | 78 + apps/ui/html/logs.html | 148 ++ apps/ui/html/settings.html | 597 +++++++ apps/ui/html/usage.html | 228 +++ apps/ui/html/wizard/index.html | 238 +++ apps/ui/html/wizard/step1.html | 95 ++ apps/ui/html/wizard/step2.html | 157 ++ apps/ui/html/wizard/step3-graphql.html | 92 + apps/ui/html/wizard/step3-grpc.html | 274 +++ apps/ui/html/wizard/step3-rest.html | 93 + apps/ui/html/wizard/step4.html | 162 ++ apps/ui/html/wizard/step5.html | 102 ++ apps/ui/html/workspace-setup.html | 327 ++++ apps/ui/icons/agents/agent.svg | 9 + apps/ui/icons/agents/eye.svg | 7 + apps/ui/icons/agents/key.svg | 6 + apps/ui/icons/general/arrow-down.svg | 6 + apps/ui/icons/general/arrow-left.svg | 6 + apps/ui/icons/general/arrow-right.svg | 6 + apps/ui/icons/general/arrow-up.svg | 6 + apps/ui/icons/general/bell.svg | 6 + apps/ui/icons/general/check-sm.svg | 6 + apps/ui/icons/general/check.svg | 6 + apps/ui/icons/general/chevron-down.svg | 6 + apps/ui/icons/general/close.svg | 6 + apps/ui/icons/general/copy.svg | 6 + apps/ui/icons/general/crank-logo.svg | 8 + apps/ui/icons/general/download.svg | 6 + apps/ui/icons/general/edit.svg | 6 + apps/ui/icons/general/external.svg | 6 + apps/ui/icons/general/grid.svg | 6 + apps/ui/icons/general/info-circle.svg | 7 + apps/ui/icons/general/info.svg | 6 + apps/ui/icons/general/logout.svg | 7 + apps/ui/icons/general/plus-sm.svg | 6 + apps/ui/icons/general/plus.svg | 6 + apps/ui/icons/general/refresh.svg | 6 + apps/ui/icons/general/search.svg | 6 + apps/ui/icons/general/settings.svg | 7 + apps/ui/icons/general/trash.svg | 6 + apps/ui/icons/general/user.svg | 6 + apps/ui/icons/general/warning.svg | 7 + apps/ui/icons/login/google.svg | 9 + apps/ui/icons/wizard/clock.svg | 7 + apps/ui/icons/wizard/code.svg | 6 + apps/ui/icons/wizard/file.svg | 6 + apps/ui/icons/wizard/graphql-protocol.svg | 12 + apps/ui/icons/wizard/grpc-protocol.svg | 8 + apps/ui/icons/wizard/reflection.svg | 7 + apps/ui/icons/wizard/rest-protocol.svg | 6 + apps/ui/icons/wizard/server.svg | 9 + apps/ui/icons/wizard/upload.svg | 6 + apps/ui/index.html | 494 +++++- apps/ui/js/agents.js | 208 +++ apps/ui/js/api-keys.js | 241 +++ apps/ui/js/catalog.js | 333 ++++ apps/ui/js/config.js | 20 + apps/ui/js/i18n.js | 289 ++++ apps/ui/js/login.js | 34 + apps/ui/js/logs.js | 487 ++++++ apps/ui/js/nav.js | 102 ++ apps/ui/js/settings.js | 73 + apps/ui/js/usage.js | 270 +++ apps/ui/js/wizard.js | 1041 ++++++++++++ apps/ui/js/workspace-setup.js | 193 +++ apps/ui/js/workspace.js | 64 + 84 files changed, 12530 insertions(+), 92 deletions(-) create mode 100644 apps/ui/css/catalog.css create mode 100644 apps/ui/css/layout.css create mode 100644 apps/ui/css/login.css create mode 100644 apps/ui/css/logs.css create mode 100644 apps/ui/css/pages.css create mode 100644 apps/ui/css/settings.css create mode 100644 apps/ui/css/usage.css create mode 100644 apps/ui/css/variables.css create mode 100644 apps/ui/css/wizard.css create mode 100644 apps/ui/css/workspace-setup.css create mode 100644 apps/ui/data/agents.json create mode 100644 apps/ui/data/keys.json create mode 100644 apps/ui/data/operations.json create mode 100644 apps/ui/html/agents.html create mode 100644 apps/ui/html/api-keys.html create mode 100644 apps/ui/html/fragments/lang-switcher.html create mode 100644 apps/ui/html/login.html create mode 100644 apps/ui/html/logs.html create mode 100644 apps/ui/html/settings.html create mode 100644 apps/ui/html/usage.html create mode 100644 apps/ui/html/wizard/index.html create mode 100644 apps/ui/html/wizard/step1.html create mode 100644 apps/ui/html/wizard/step2.html create mode 100644 apps/ui/html/wizard/step3-graphql.html create mode 100644 apps/ui/html/wizard/step3-grpc.html create mode 100644 apps/ui/html/wizard/step3-rest.html create mode 100644 apps/ui/html/wizard/step4.html create mode 100644 apps/ui/html/wizard/step5.html create mode 100644 apps/ui/html/workspace-setup.html create mode 100644 apps/ui/icons/agents/agent.svg create mode 100644 apps/ui/icons/agents/eye.svg create mode 100644 apps/ui/icons/agents/key.svg create mode 100644 apps/ui/icons/general/arrow-down.svg create mode 100644 apps/ui/icons/general/arrow-left.svg create mode 100644 apps/ui/icons/general/arrow-right.svg create mode 100644 apps/ui/icons/general/arrow-up.svg create mode 100644 apps/ui/icons/general/bell.svg create mode 100644 apps/ui/icons/general/check-sm.svg create mode 100644 apps/ui/icons/general/check.svg create mode 100644 apps/ui/icons/general/chevron-down.svg create mode 100644 apps/ui/icons/general/close.svg create mode 100644 apps/ui/icons/general/copy.svg create mode 100644 apps/ui/icons/general/crank-logo.svg create mode 100644 apps/ui/icons/general/download.svg create mode 100644 apps/ui/icons/general/edit.svg create mode 100644 apps/ui/icons/general/external.svg create mode 100644 apps/ui/icons/general/grid.svg create mode 100644 apps/ui/icons/general/info-circle.svg create mode 100644 apps/ui/icons/general/info.svg create mode 100644 apps/ui/icons/general/logout.svg create mode 100644 apps/ui/icons/general/plus-sm.svg create mode 100644 apps/ui/icons/general/plus.svg create mode 100644 apps/ui/icons/general/refresh.svg create mode 100644 apps/ui/icons/general/search.svg create mode 100644 apps/ui/icons/general/settings.svg create mode 100644 apps/ui/icons/general/trash.svg create mode 100644 apps/ui/icons/general/user.svg create mode 100644 apps/ui/icons/general/warning.svg create mode 100644 apps/ui/icons/login/google.svg create mode 100644 apps/ui/icons/wizard/clock.svg create mode 100644 apps/ui/icons/wizard/code.svg create mode 100644 apps/ui/icons/wizard/file.svg create mode 100644 apps/ui/icons/wizard/graphql-protocol.svg create mode 100644 apps/ui/icons/wizard/grpc-protocol.svg create mode 100644 apps/ui/icons/wizard/reflection.svg create mode 100644 apps/ui/icons/wizard/rest-protocol.svg create mode 100644 apps/ui/icons/wizard/server.svg create mode 100644 apps/ui/icons/wizard/upload.svg create mode 100644 apps/ui/js/agents.js create mode 100644 apps/ui/js/api-keys.js create mode 100644 apps/ui/js/catalog.js create mode 100644 apps/ui/js/config.js create mode 100644 apps/ui/js/i18n.js create mode 100644 apps/ui/js/login.js create mode 100644 apps/ui/js/logs.js create mode 100644 apps/ui/js/nav.js create mode 100644 apps/ui/js/settings.js create mode 100644 apps/ui/js/usage.js create mode 100644 apps/ui/js/wizard.js create mode 100644 apps/ui/js/workspace-setup.js create mode 100644 apps/ui/js/workspace.js diff --git a/TASKS.md b/TASKS.md index 38b6cfb..959ed3d 100644 --- a/TASKS.md +++ b/TASKS.md @@ -2,19 +2,19 @@ ## Current -### `feat/observability-api` +### `feat/alpine-ui` -Status: completed +Status: in_progress DoD: -- `Logs` page и `Usage` page имеют backend-контракт на реальных данных -- `test-runs` и `MCP tools/call` пишут продуктовые invocation logs -- usage endpoints отдают summary, timeline, breakdown по operation и agent -- observability не смешана с application logs +- `apps/ui` заменен на новый Alpine.js UI из `test-ui` +- `test-ui` остается нетронутым как fallback +- UI продолжает раздаваться как статический контейнер через текущий deployment path +- legacy React/Vite артефакты удалены из `apps/ui` ## Next -- `feat/alpine-ui` +- `feat/demo-assets` ## Backlog diff --git a/apps/ui/Dockerfile b/apps/ui/Dockerfile index 8be8867..419229c 100644 --- a/apps/ui/Dockerfile +++ b/apps/ui/Dockerfile @@ -2,6 +2,11 @@ FROM nginx:1.27-alpine COPY apps/ui/nginx.conf /etc/nginx/conf.d/default.conf COPY apps/ui/index.html /usr/share/nginx/html/index.html +COPY apps/ui/css /usr/share/nginx/html/css +COPY apps/ui/data /usr/share/nginx/html/data +COPY apps/ui/html /usr/share/nginx/html/html +COPY apps/ui/icons /usr/share/nginx/html/icons +COPY apps/ui/js /usr/share/nginx/html/js COPY Crank.png /usr/share/nginx/html/Crank.png EXPOSE 3000 diff --git a/apps/ui/css/catalog.css b/apps/ui/css/catalog.css new file mode 100644 index 0000000..20003fd --- /dev/null +++ b/apps/ui/css/catalog.css @@ -0,0 +1,579 @@ +/* ── Page header ── */ +.page-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 32px; +} +.page-heading { + font-size: 24px; + font-weight: 700; + letter-spacing: -0.5px; + color: var(--text-primary); + margin-bottom: 4px; +} +.page-subheading { font-size: 14px; color: var(--text-muted); } + +.btn-new { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 9px 18px; + border-radius: var(--radius); + background: var(--accent); + color: #fff; + font-size: 13.5px; + font-weight: 600; + border: 1px solid var(--accent-dim); + box-shadow: 0 1px 4px rgba(13,148,136,0.4); + transition: all 0.15s; + white-space: nowrap; + text-decoration: none; +} +.btn-new:hover { + background: var(--accent-dim); + box-shadow: 0 2px 10px rgba(13,148,136,0.5); + transform: translateY(-1px); +} + +/* ── Stats ── */ +.stats-row { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 1px; + background: var(--border); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + overflow: hidden; + margin-bottom: 28px; +} +.stat-card { background: var(--bg-surface); padding: 18px 22px; } +.stat-label { + font-size: 11px; + color: var(--text-muted); + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + margin-bottom: 6px; +} +.stat-value { + font-size: 26px; + font-weight: 700; + letter-spacing: -1px; + color: var(--text-primary); +} +.stat-delta { font-size: 12px; color: var(--text-muted); margin-top: 3px; } +.stat-delta.up { color: var(--green); } +.stat-delta.down { color: var(--red); } + +/* ── Tabs ── */ +.tab-row { + display: flex; + align-items: center; + border-bottom: 1px solid var(--border); + margin-bottom: 20px; +} +.tab-item { + display: flex; + align-items: center; + gap: 7px; + padding: 10px 18px; + font-size: 14px; + font-weight: 450; + color: var(--text-muted); + cursor: pointer; + border: none; + background: none; + border-bottom: 2px solid transparent; + margin-bottom: -1px; + transition: color 0.1s; +} +.tab-item:hover { color: var(--text-secondary); } +.tab-item.active { + color: var(--text-primary); + font-weight: 600; + border-bottom-color: var(--accent-bright); +} +.tab-count { + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--bg-overlay); + border: 1px solid var(--border); + border-radius: 10px; + padding: 0 6px; + height: 18px; + font-size: 11px; + font-weight: 600; + color: var(--text-muted); + min-width: 20px; +} +.tab-item.active .tab-count { + background: var(--accent); + color: #fff; + border-color: var(--accent-dim); +} + +/* ── Filter bar ── */ +.filter-bar { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; + flex-wrap: wrap; +} + +.search-box { + display: flex; + align-items: center; + gap: 8px; + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 7px 12px; + width: 220px; + transition: border-color 0.15s, box-shadow 0.15s; +} +.search-box:focus-within { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-ring); +} +.search-box input { + border: none; + background: none; + outline: none; + font-size: 13.5px; + color: var(--text-primary); + width: 100%; + font-family: inherit; +} +.search-box input::placeholder { color: var(--text-muted); } +.search-box svg { color: var(--text-muted); flex-shrink: 0; } + +/* ── Dropdown filter chip ── */ +.filter-dropdown { position: relative; } + +.filter-chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 7px 12px; + border-radius: var(--radius); + border: 1px solid var(--border); + background: var(--bg-surface); + font-size: 13px; + color: var(--text-secondary); + transition: all 0.1s; + white-space: nowrap; +} +.filter-chip:hover { border-color: var(--bg-muted); background: var(--bg-overlay); color: var(--text-primary); } +.filter-chip.has-value { + border-color: var(--accent); + color: var(--accent-bright); + background: var(--accent-glow); +} +.filter-chip .chevron { + opacity: 0.6; + transition: transform 0.15s; +} +.filter-chip.open .chevron { transform: rotate(180deg); } + +.dropdown-menu { + position: absolute; + top: calc(100% + 6px); + left: 0; + min-width: 180px; + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow); + z-index: 150; + overflow: hidden; + padding: 4px; +} +.dropdown-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 10px; + border-radius: var(--radius-sm); + font-size: 13.5px; + color: var(--text-secondary); + border: none; + background: none; + width: 100%; + text-align: left; + cursor: pointer; + transition: all 0.1s; +} +.dropdown-item:hover { background: var(--bg-overlay); color: var(--text-primary); } +.dropdown-item.selected { color: var(--accent-bright); } +.dropdown-item .check { + opacity: 0; + color: var(--accent); +} +.dropdown-item.selected .check { opacity: 1; } +.dropdown-divider { + height: 1px; + background: var(--border-subtle); + margin: 4px 0; +} +.dropdown-clear { + display: flex; + align-items: center; + padding: 7px 10px; + font-size: 12px; + color: var(--text-muted); + border: none; + background: none; + width: 100%; + text-align: left; + cursor: pointer; + border-radius: var(--radius-sm); + transition: all 0.1s; +} +.dropdown-clear:hover { color: var(--red); background: var(--red-bg); } + +/* ── Filter right side ── */ +.filter-right { + display: flex; + align-items: center; + gap: 10px; + margin-left: auto; +} +.results-count { font-size: 13px; color: var(--text-muted); white-space: nowrap; } + +.sort-dropdown { position: relative; } +.sort-chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 7px 12px; + border-radius: var(--radius); + border: 1px solid var(--border); + background: var(--bg-surface); + font-size: 13px; + color: var(--text-secondary); + transition: all 0.1s; + white-space: nowrap; +} +.sort-chip:hover { border-color: var(--bg-muted); background: var(--bg-overlay); color: var(--text-primary); } +.sort-chip .chevron { opacity: 0.6; transition: transform 0.15s; } +.sort-chip.open .chevron { transform: rotate(180deg); } + +/* ── Table ── */ +.table-wrap { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + overflow: hidden; +} +.table-wrap table { + width: 100%; + border-collapse: collapse; +} +.table-wrap thead tr { + background: var(--bg-canvas); + border-bottom: 1px solid var(--border); +} +.table-wrap th { + padding: 10px 16px; + text-align: left; + font-size: 11.5px; + font-weight: 700; + color: var(--text-muted); + letter-spacing: 0.06em; + text-transform: uppercase; + white-space: nowrap; + user-select: none; +} +.th-sortable { cursor: pointer; } +.th-sortable:hover { color: var(--text-secondary); } +.th-sortable.sort-active { color: var(--text-secondary); } +.sort-arrow { margin-left: 4px; opacity: 0.5; font-size: 11px; } +.sort-arrow.active { opacity: 1; color: var(--accent-bright); } + +.table-wrap tbody tr { + border-bottom: 1px solid var(--border-subtle); + transition: background 0.1s; +} +.table-wrap tbody tr:last-child { border-bottom: none; } +.table-wrap tbody tr:hover { background: var(--bg-overlay); } +.table-wrap td { padding: 14px 16px; vertical-align: middle; } + +.op-name { font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 13px; font-weight: 500; color: var(--text-primary); } +.op-display { font-size: 12px; color: var(--text-muted); margin-top: 3px; } + +/* ── Badges ── */ +.badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 8px; + border-radius: var(--radius-sm); + font-size: 11.5px; + font-weight: 700; + white-space: nowrap; + border: 1px solid; + letter-spacing: 0.02em; +} +.badge-rest { color: var(--blue); background: var(--blue-bg); border-color: var(--blue-border); } +.badge-graphql { color: var(--purple); background: var(--purple-bg); border-color: var(--purple-border); } +.badge-grpc { color: #14b8a6; background: rgba(13,148,136,0.1); border-color: rgba(13,148,136,0.25); } + +.status-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 3px 9px; + border-radius: var(--radius-sm); + font-size: 12.5px; + font-weight: 500; +} +.status-dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; +} +.status-active { color: var(--green); background: var(--green-bg); } +.status-active .status-dot { background: var(--green); box-shadow: 0 0 0 2px rgba(63,185,80,0.2); } +.status-draft { color: var(--amber); background: var(--amber-bg); } +.status-draft .status-dot { background: var(--amber); } +.status-error { color: var(--red); background: var(--red-bg); } +.status-error .status-dot { background: var(--red); box-shadow: 0 0 0 2px rgba(248,81,73,0.2); } +.status-inactive { color: var(--text-muted); background: var(--bg-overlay); } +.status-inactive .status-dot { background: var(--text-muted); } + +.target-url-cell { + display: flex; + align-items: center; + gap: 6px; + font-family: 'JetBrains Mono', 'Fira Code', monospace; + font-size: 12px; + color: var(--text-secondary); + max-width: 280px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.target-url-cell a { color: inherit; text-decoration: none; } +.target-url-cell svg { flex-shrink: 0; opacity: 0.5; } + +.date-cell { font-size: 13px; color: var(--text-muted); white-space: nowrap; } + +.row-actions { display: flex; align-items: center; gap: 4px; justify-content: flex-end; } +.row-btn { + padding: 5px 11px; + border-radius: var(--radius-sm); + border: 1px solid var(--border); + background: var(--bg-surface); + font-size: 12.5px; + color: var(--text-secondary); + transition: all 0.1s; +} +.row-btn:hover { border-color: var(--bg-muted); color: var(--text-primary); background: var(--bg-overlay); } +.row-btn.danger:hover { border-color: var(--red-border); color: var(--red); background: var(--red-bg); } + +.row-btn-edit { + width: 34px; + height: 34px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius); +} + +/* ── Empty state ── */ +.empty-state { + padding: 60px 20px; + text-align: center; + color: var(--text-muted); +} +.empty-state h3 { font-size: 16px; color: var(--text-secondary); margin-bottom: 8px; } +.empty-state p { font-size: 13.5px; } + +/* ── Table footer / pagination ── */ +.table-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 20px; + border-top: 1px solid var(--border); + background: var(--bg-canvas); +} +.table-footer-info { font-size: 13px; color: var(--text-muted); } + +.pagination { display: flex; align-items: center; gap: 4px; } +.page-btn { + width: 32px; + height: 32px; + border-radius: var(--radius); + border: 1px solid var(--border); + background: var(--bg-surface); + font-size: 13px; + color: var(--text-secondary); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.1s; +} +.page-btn:hover:not(:disabled) { border-color: var(--bg-muted); color: var(--text-primary); } +.page-btn.active { background: var(--accent); color: #fff; border-color: var(--accent-dim); font-weight: 600; } +.page-btn:disabled { opacity: 0.3; cursor: not-allowed; } + +/* ── Banner ── */ +.banner { + display: flex; + align-items: center; + gap: 10px; + padding: 11px 16px; + border-radius: var(--radius); + background: rgba(13,148,136,0.07); + border: 1px solid rgba(13,148,136,0.2); + color: var(--accent-bright); + font-size: 13.5px; + margin-bottom: 24px; +} +.banner-close { + margin-left: auto; + background: none; + border: none; + color: var(--accent-bright); + font-size: 16px; + opacity: 0.6; + line-height: 1; +} +.banner-close:hover { opacity: 1; } + +/* ═══════════════ RESPONSIVE ═══════════════ */ + +/* Tablet: ≤ 960px */ +@media (max-width: 960px) { + .stats-row { grid-template-columns: repeat(2, 1fr); } + .col-url { display: none; } + .results-count { display: none; } +} + +/* Narrow / mobile: ≤ 720px */ +@media (max-width: 720px) { + /* --- Скрываем колонку CREATED --- */ + .col-created { display: none; } + + /* --- Filter bar → grid --- */ + .filter-bar { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 6px; + } + /* Search во всю ширину */ + .search-box { + grid-column: 1 / -1; + width: 100%; + } + /* Каждый dropdown занимает ячейку */ + .filter-dropdown { + width: 100%; + } + .filter-chip { + width: 100%; + justify-content: space-between; + } + /* Sort справа, во всю строку */ + .filter-right { + grid-column: 1 / -1; + margin-left: 0; + justify-content: flex-end; + } + .sort-chip { white-space: nowrap; } + + /* action column compact */ + .row-actions { justify-content: center; } + + /* --- Таблица --- */ + .table-wrap td { padding: 12px 10px; } + .table-wrap th { padding: 8px 10px; } +} + +/* Mobile: ≤ 480px */ +@media (max-width: 480px) { + .tab-item { padding: 10px 8px; font-size: 13px; gap: 5px; } + + .page-header { flex-direction: column; gap: 12px; align-items: flex-start; } + .btn-new { width: 100%; justify-content: center; } + +} + +/* ── Active filter chips ── */ +.active-filters { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; + padding: 8px 0 4px; +} +.active-filter-chip { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 8px 3px 10px; + background: rgba(13,148,136,0.12); + border: 1px solid rgba(13,148,136,0.3); + border-radius: 20px; + font-size: 12px; + color: var(--accent); +} +.active-filter-chip-remove { + display: flex; + align-items: center; + background: none; + border: none; + padding: 1px; + cursor: pointer; + color: var(--accent); + opacity: 0.7; + border-radius: 50%; + transition: opacity 0.1s; +} +.active-filter-chip-remove:hover { opacity: 1; } +.active-filter-clear-all { + background: none; + border: none; + font-size: 12px; + color: var(--text-muted); + cursor: pointer; + padding: 3px 6px; + border-radius: 4px; + transition: color 0.1s; +} +.active-filter-clear-all:hover { color: var(--text-secondary); } + +/* ── Operation agent badges ── */ +.op-agent-badges { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 4px; +} +.op-agent-badge { + display: inline-block; + padding: 1px 6px; + font-size: 10px; + font-weight: 500; + background: rgba(99,102,241,0.12); + border: 1px solid rgba(99,102,241,0.25); + border-radius: 3px; + color: #818cf8; + white-space: nowrap; +} + +/* ── Row delete button ── */ +.row-btn-delete { + color: var(--text-muted); + transition: color 0.1s, background 0.1s; +} +.row-btn-delete:hover { + color: var(--red, #f87171); + background: rgba(248,113,113,0.08); +} diff --git a/apps/ui/css/layout.css b/apps/ui/css/layout.css new file mode 100644 index 0000000..2092915 --- /dev/null +++ b/apps/ui/css/layout.css @@ -0,0 +1,231 @@ +/* ── Navbar ── */ +.navbar { + height: 60px; + background: var(--bg-surface); + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + padding: 0 40px; + position: sticky; + top: 0; + z-index: 100; + box-shadow: 0 1px 0 var(--border-subtle), 0 4px 16px rgba(0,0,0,0.4); +} + +.nav-logo { + display: flex; + align-items: center; + gap: 9px; + margin-right: 36px; + text-decoration: none; +} +.nav-logo-mark { + width: 28px; + height: 28px; + background: linear-gradient(135deg, #0d9488, #0891b2); + border-radius: 7px; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 13px; + font-weight: 800; + flex-shrink: 0; +} +.nav-logo-text { + font-size: 15px; + font-weight: 700; + letter-spacing: -0.4px; + color: var(--text-primary); +} + +.nav-links { + display: flex; + align-items: center; + gap: 2px; + flex: 1; +} +.nav-link { + padding: 6px 14px; + border-radius: var(--radius); + font-size: 14px; + font-weight: 450; + color: var(--text-muted); + text-decoration: none; + border: none; + background: none; + transition: all 0.1s; +} +.nav-link:hover { background: var(--bg-overlay); color: var(--text-secondary); } +.nav-link.active { color: var(--text-primary); font-weight: 550; background: var(--bg-overlay); } + +.nav-right { + display: flex; + align-items: center; + gap: 10px; + margin-left: auto; +} +.nav-kbd { + display: flex; + align-items: center; + gap: 4px; + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 4px 9px; + font-size: 12px; + color: var(--text-muted); + background: var(--bg-overlay); + cursor: pointer; +} +.nav-icon-btn { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius); + border: none; + background: none; + color: var(--text-muted); + transition: all 0.1s; + position: relative; +} +.nav-icon-btn:hover { background: var(--bg-overlay); color: var(--text-secondary); } +.nav-divider { width: 1px; height: 20px; background: var(--border); } + +.nav-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background: linear-gradient(135deg, #0d9488, #6366f1); + color: #fff; + font-size: 11.5px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border: 2px solid transparent; + transition: border-color 0.15s; +} +.nav-avatar:hover { border-color: var(--accent); } + +/* ── User dropdown ── */ +.user-menu { + position: relative; +} +.user-dropdown { + position: absolute; + top: calc(100% + 8px); + right: 0; + min-width: 180px; + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow); + overflow: hidden; + z-index: 200; +} +.user-dropdown-header { + padding: 12px 14px; + border-bottom: 1px solid var(--border-subtle); +} +.user-dropdown-name { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); +} +.user-dropdown-role { + font-size: 11.5px; + color: var(--text-muted); + margin-top: 2px; +} +.user-dropdown-item { + display: flex; + align-items: center; + gap: 9px; + padding: 9px 14px; + font-size: 13.5px; + color: var(--text-secondary); + border: none; + background: none; + width: 100%; + text-align: left; + transition: all 0.1s; +} +.user-dropdown-item:hover { background: var(--bg-overlay); color: var(--text-primary); } +.user-dropdown-item.danger:hover { background: var(--red-bg); color: var(--red); } +.user-dropdown-item svg { flex-shrink: 0; opacity: 0.7; } + +/* ── Page container ── */ +.page { + max-width: 1160px; + margin: 0 auto; + padding: 36px 40px 60px; +} + +/* ── Hamburger button (hidden on desktop) ── */ +.nav-hamburger { + display: none; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 5px; + width: 32px; + height: 32px; + border: none; + background: none; + cursor: pointer; + padding: 4px; + border-radius: var(--radius); + transition: background 0.1s; + order: 10; /* after other right items */ +} +.nav-hamburger:hover { background: var(--bg-overlay); } +.nav-hamburger span { + display: block; + width: 18px; + height: 1.5px; + background: var(--text-muted); + border-radius: 1px; + transition: all 0.2s; + transform-origin: center; +} +.nav-hamburger.open span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); } +.nav-hamburger.open span:nth-child(2) { opacity: 0; } +.nav-hamburger.open span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); } + +/* ── Mobile nav dropdown ── */ +.mobile-nav { + display: none; + position: sticky; + top: 60px; + z-index: 99; + background: var(--bg-surface); + border-bottom: 1px solid var(--border); + padding: 8px 16px 12px; + box-shadow: 0 4px 16px rgba(0,0,0,0.4); +} +.mobile-nav-link { + display: block; + padding: 10px 12px; + border-radius: var(--radius); + font-size: 14px; + font-weight: 500; + color: var(--text-muted); + text-decoration: none; + transition: all 0.1s; +} +.mobile-nav-link:hover { background: var(--bg-overlay); color: var(--text-secondary); } +.mobile-nav-link.active { color: var(--text-primary); background: var(--bg-overlay); } + +/* ── Responsive breakpoints ── */ +@media (max-width: 720px) { + .navbar { padding: 0 16px; } + .nav-links { display: none; } + .nav-kbd { display: none; } + .nav-hamburger { display: flex; } + .mobile-nav { display: block; } + + .page { padding: 20px 16px 40px; } +} diff --git a/apps/ui/css/login.css b/apps/ui/css/login.css new file mode 100644 index 0000000..3cb948c --- /dev/null +++ b/apps/ui/css/login.css @@ -0,0 +1,192 @@ +.login-page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + background: + radial-gradient(ellipse 70% 50% at 50% -20%, rgba(13,148,136,0.12) 0%, transparent 70%), + var(--bg-canvas); +} + +.login-card { + width: 100%; + max-width: 380px; +} + +.login-logo { + display: flex; + align-items: center; + gap: 10px; + justify-content: center; + margin-bottom: 36px; +} + +.login-logo-mark { + width: 36px; + height: 36px; + background: linear-gradient(135deg, #0d9488, #0891b2); + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 17px; + font-weight: 800; +} + +.login-logo-text { + font-size: 20px; + font-weight: 700; + letter-spacing: -0.5px; + color: var(--text-primary); +} + +.login-box { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: 14px; + padding: 32px; + box-shadow: 0 8px 40px rgba(0,0,0,0.4); +} + +.login-heading { + font-size: 20px; + font-weight: 700; + letter-spacing: -0.4px; + color: var(--text-primary); + margin-bottom: 4px; +} + +.login-sub { + font-size: 13.5px; + color: var(--text-muted); + margin-bottom: 28px; +} + +.field { + margin-bottom: 16px; +} + +.field-label { + display: block; + font-size: 12.5px; + font-weight: 500; + color: var(--text-secondary); + margin-bottom: 6px; +} + +.field-input { + width: 100%; + padding: 9px 12px; + background: var(--bg-canvas); + border: 1px solid var(--border); + border-radius: 7px; + font-family: 'Inter', sans-serif; + font-size: 13.5px; + color: var(--text-primary); + outline: none; + transition: border-color 0.15s, box-shadow 0.15s; +} + +.field-input::placeholder { color: var(--text-muted); } + +.field-input:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(13,148,136,0.25); +} + +.field-footer { + display: flex; + justify-content: flex-end; + margin-top: 5px; +} + +.field-link { + font-size: 12px; + color: var(--accent); + text-decoration: none; +} + +.field-link:hover { text-decoration: underline; } + +.btn-signin { + width: 100%; + padding: 10px; + margin-top: 8px; + background: var(--accent); + color: #fff; + border: 1px solid var(--accent-dim); + border-radius: 7px; + font-family: 'Inter', sans-serif; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s; + box-shadow: 0 1px 4px rgba(13,148,136,0.4); +} + +.btn-signin:hover { + background: var(--accent-dim); + box-shadow: 0 2px 10px rgba(13,148,136,0.5), 0 0 0 3px rgba(13,148,136,0.25); +} + +.btn-signin:active { transform: translateY(1px); } + +.login-error { + display: none; + background: var(--red-bg); + border: 1px solid var(--red-border); + border-radius: 7px; + padding: 10px 14px; + font-size: 13px; + color: var(--red); + margin-bottom: 16px; +} + +.login-footer { + text-align: center; + margin-top: 20px; + font-size: 12.5px; + color: var(--text-muted); +} + +.login-footer a { color: var(--accent); text-decoration: none; } +.login-footer a:hover { text-decoration: underline; } + +.login-divider { + display: flex; + align-items: center; + gap: 12px; + margin: 22px 0; +} + +.login-divider-line { flex: 1; height: 1px; background: var(--border); } +.login-divider-text { font-size: 11.5px; color: var(--text-muted); white-space: nowrap; } + +.btn-sso { + width: 100%; + padding: 9px; + background: var(--bg-overlay); + color: var(--text-secondary); + border: 1px solid var(--border); + border-radius: 7px; + font-family: 'Inter', sans-serif; + font-size: 13.5px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s; + display: flex; + align-items: center; + justify-content: center; + gap: 9px; +} + +.btn-sso:hover { background: var(--bg-muted); color: var(--text-primary); } + +.version-badge { + text-align: center; + margin-top: 28px; + font-size: 11.5px; + color: var(--text-muted); +} diff --git a/apps/ui/css/logs.css b/apps/ui/css/logs.css new file mode 100644 index 0000000..e3f4a04 --- /dev/null +++ b/apps/ui/css/logs.css @@ -0,0 +1,125 @@ +.log-toolbar { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 20px; + border-bottom: 1px solid var(--border-subtle); + flex-wrap: wrap; +} + +.live-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--green); + animation: pulse 1.8s ease-in-out infinite; + flex-shrink: 0; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(63,185,80,0.6); } + 50% { opacity: 0.7; box-shadow: 0 0 0 5px rgba(63,185,80,0); } +} + +.live-label { + font-size: 11.5px; + font-weight: 600; + color: var(--green); + letter-spacing: 0.2px; +} + +.toolbar-sep { width: 1px; height: 16px; background: var(--border); flex-shrink: 0; } + +.time-range-select { + background: var(--bg-canvas); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 5px 28px 5px 10px; + font-family: 'Inter', sans-serif; + font-size: 12.5px; + color: var(--text-secondary); + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12'%3E%3Cpath fill='%236e7681' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 8px center; + cursor: pointer; + outline: none; +} + +.time-range-select:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-ring); +} + +.log-op-name { + font-family: 'JetBrains Mono', monospace; + font-size: 11.5px; + color: var(--text-muted); + background: var(--bg-overlay); + border: 1px solid var(--border-subtle); + padding: 1px 7px; + border-radius: 4px; + white-space: nowrap; +} + +.log-status { + font-family: 'JetBrains Mono', monospace; + font-size: 11.5px; + font-weight: 600; +} + +.log-status.ok { color: var(--green); } +.log-status.err { color: var(--red); } +.log-status.warn { color: var(--amber); } + +.log-entry-expanded { + padding: 10px 20px 16px calc(20px + 152px + 12px + 58px + 12px); + background: var(--bg-canvas); + border-bottom: 1px solid var(--border-subtle); + display: none; +} + +.log-entry-expanded.open { display: block; } + +.log-detail-block { + background: #161b22; + border: 1px solid var(--border); + border-radius: 6px; + padding: 12px 14px; + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + color: #c9d1d9; + line-height: 1.65; + white-space: pre-wrap; + word-break: break-all; +} + +.log-detail-label { + font-size: 11px; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; + color: var(--text-muted); + margin-bottom: 6px; + margin-top: 12px; +} + +.log-detail-label:first-child { margin-top: 0; } + +.refresh-btn { + margin-left: auto; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 12px; + background: var(--bg-overlay); + border: 1px solid var(--border); + border-radius: var(--radius); + font-size: 12.5px; + color: var(--text-muted); + cursor: pointer; + font-family: 'Inter', sans-serif; + transition: all 0.12s; +} + +.refresh-btn:hover { color: var(--text-secondary); background: var(--bg-muted); } diff --git a/apps/ui/css/pages.css b/apps/ui/css/pages.css new file mode 100644 index 0000000..1fb23f3 --- /dev/null +++ b/apps/ui/css/pages.css @@ -0,0 +1,1458 @@ +/* ══════════════════════════════════════════════════ + SHARED INNER-PAGE STYLES + Used by: api-keys, logs, usage, settings +══════════════════════════════════════════════════ */ + +/* ── Dropdown divider (used in navbar and elsewhere) ── */ +.dropdown-divider { + height: 1px; + background: var(--border-subtle); + margin: 4px 0; +} + +/* ══════════════════════════════════════════════════ + PAGE HEADER +══════════════════════════════════════════════════ */ +.page-header { + display: flex; + align-items: flex-start; + gap: 16px; + margin-bottom: 28px; +} + +.page-header-text { flex: 1; } + +.page-title { + font-size: 22px; + font-weight: 700; + letter-spacing: -0.4px; + color: var(--text-primary); + line-height: 1.2; +} + +.page-subtitle { + font-size: 13.5px; + color: var(--text-muted); + margin-top: 4px; + line-height: 1.5; +} + +.page-header-actions { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; +} + +/* ══════════════════════════════════════════════════ + BUTTONS +══════════════════════════════════════════════════ */ +.btn-primary { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 8px 16px; + background: var(--accent); + color: #fff; + border: 1px solid var(--accent-dim); + border-radius: var(--radius); + font-size: 13.5px; + font-weight: 500; + font-family: 'Inter', sans-serif; + cursor: pointer; + transition: all 0.15s; + white-space: nowrap; +} + +.btn-primary:hover { + background: var(--accent-dim); + box-shadow: 0 0 0 3px var(--accent-ring); +} + +.btn-secondary { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 8px 16px; + background: var(--bg-overlay); + color: var(--text-secondary); + border: 1px solid var(--border); + border-radius: var(--radius); + font-size: 13.5px; + font-weight: 500; + font-family: 'Inter', sans-serif; + cursor: pointer; + transition: all 0.15s; + white-space: nowrap; +} + +.btn-secondary:hover { background: var(--bg-muted); color: var(--text-primary); } + +.btn-danger { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 8px 16px; + background: transparent; + color: var(--red); + border: 1px solid rgba(248, 81, 73, 0.3); + border-radius: var(--radius); + font-size: 13.5px; + font-weight: 500; + font-family: 'Inter', sans-serif; + cursor: pointer; + transition: all 0.15s; +} + +.btn-danger:hover { background: var(--red-bg); border-color: var(--red-border); } + +.btn-icon { + width: 30px; + height: 30px; + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--bg-overlay); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text-muted); + cursor: pointer; + transition: all 0.15s; + flex-shrink: 0; +} + +.btn-icon:hover { background: var(--bg-muted); color: var(--text-secondary); } + +.btn-icon.danger:hover { background: var(--red-bg); color: var(--red); border-color: var(--red-border); } + +/* ══════════════════════════════════════════════════ + SECTION CARD +══════════════════════════════════════════════════ */ +.section-card { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + overflow: hidden; + margin-bottom: 20px; +} + +.section-card-header { + padding: 14px 20px; + border-bottom: 1px solid var(--border-subtle); + display: flex; + align-items: center; + gap: 12px; +} + +.section-card-title { + font-size: 13.5px; + font-weight: 600; + color: var(--text-primary); + flex: 1; +} + +.section-card-subtitle { + font-size: 12px; + color: var(--text-muted); + margin-top: 1px; +} + +.section-card-body { padding: 20px; } + +/* ══════════════════════════════════════════════════ + CALLOUT / INFO BOX +══════════════════════════════════════════════════ */ +.callout { + display: flex; + gap: 12px; + padding: 13px 16px; + border-radius: var(--radius); + margin-bottom: 20px; + border: 1px solid; + font-size: 13px; + line-height: 1.55; +} + +.callout.info { + background: rgba(88, 166, 255, 0.07); + border-color: var(--blue-border); + color: var(--text-secondary); +} + +.callout.warning { + background: var(--amber-bg); + border-color: var(--amber-border); + color: var(--text-secondary); +} + +.callout.danger { + background: var(--red-bg); + border-color: var(--red-border); + color: var(--text-secondary); +} + +.callout-icon { flex-shrink: 0; margin-top: 1px; } +.callout strong { color: var(--text-primary); } + +/* ══════════════════════════════════════════════════ + STAT CARDS +══════════════════════════════════════════════════ */ +.stats-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 12px; + margin-bottom: 24px; +} + +.stat-card { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 16px 18px; +} + +.stat-label { + font-size: 11.5px; + font-weight: 500; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 8px; +} + +.stat-value { + font-size: 28px; + font-weight: 700; + letter-spacing: -1px; + color: var(--text-primary); + line-height: 1; + margin-bottom: 4px; + font-variant-numeric: tabular-nums; +} + +.stat-delta { + font-size: 12px; + color: var(--text-muted); + display: flex; + align-items: center; + gap: 4px; +} + +.stat-delta.up { color: var(--green); } +.stat-delta.down { color: var(--red); } + +/* ══════════════════════════════════════════════════ + DATA TABLE +══════════════════════════════════════════════════ */ +.data-table { + width: 100%; + border-collapse: collapse; + font-size: 13.5px; +} + +.data-table th { + padding: 10px 16px; + text-align: left; + font-size: 11.5px; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.4px; + background: var(--bg-canvas); + border-bottom: 1px solid var(--border); + white-space: nowrap; +} + +.data-table td { + padding: 12px 16px; + color: var(--text-secondary); + border-bottom: 1px solid var(--border-subtle); + vertical-align: middle; +} + +.data-table tbody tr:last-child td { border-bottom: none; } + +.data-table tbody tr:hover td { background: var(--bg-overlay); } + +.data-table .col-name { color: var(--text-primary); font-weight: 500; } +.data-table .col-mono { + font-family: 'JetBrains Mono', monospace; + font-size: 12.5px; + color: var(--text-secondary); +} + +.data-table .col-actions { + text-align: right; + white-space: nowrap; +} + +.data-table .col-actions { display: flex; justify-content: flex-end; gap: 6px; } + +/* ══════════════════════════════════════════════════ + BADGES +══════════════════════════════════════════════════ */ +.badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 4px; + font-size: 11.5px; + font-weight: 500; + border: 1px solid; + white-space: nowrap; +} + +.badge-active { background: var(--green-bg); border-color: var(--green-border); color: var(--green); } +.badge-revoked { background: var(--red-bg); border-color: var(--red-border); color: var(--red); } +.badge-draft { background: var(--amber-bg); border-color: var(--amber-border); color: var(--amber); } +.badge-scope { + background: var(--bg-overlay); + border-color: var(--border); + color: var(--text-muted); + font-size: 10.5px; + padding: 1px 6px; +} + +.log-level { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 2px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.3px; + text-transform: uppercase; + border: 1px solid; + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.log-level.info { background: rgba(88,166,255,0.08); border-color: var(--blue-border); color: var(--blue); } +.log-level.warn { background: var(--amber-bg); border-color: var(--amber-border); color: var(--amber); } +.log-level.error { background: var(--red-bg); border-color: var(--red-border); color: var(--red); } +.log-level.debug { background: var(--bg-overlay); border-color: var(--border); color: var(--text-muted); } + +/* ══════════════════════════════════════════════════ + PAGE TABS +══════════════════════════════════════════════════ */ +.page-tabs { + display: flex; + gap: 2px; + border-bottom: 1px solid var(--border); + margin-bottom: 20px; +} + +.page-tab { + padding: 8px 16px; + font-size: 13.5px; + font-weight: 450; + color: var(--text-muted); + border: none; + background: none; + cursor: pointer; + border-bottom: 2px solid transparent; + margin-bottom: -1px; + transition: all 0.15s; + font-family: 'Inter', sans-serif; + display: flex; + align-items: center; + gap: 7px; +} + +.page-tab:hover { color: var(--text-secondary); } +.page-tab.active { color: var(--text-primary); border-bottom-color: var(--accent); font-weight: 550; } + +.page-tab-count { + font-size: 11px; + background: var(--bg-overlay); + border: 1px solid var(--border); + border-radius: 10px; + padding: 0 6px; + color: var(--text-muted); + font-weight: 500; + min-width: 18px; + text-align: center; +} + +.page-tab.active .page-tab-count { + background: var(--accent-glow); + border-color: rgba(13,148,136,0.2); + color: var(--accent); +} + +/* ══════════════════════════════════════════════════ + FILTER BAR +══════════════════════════════════════════════════ */ +.filter-bar { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; + flex-wrap: wrap; +} + +.filter-bar-search { + position: relative; + flex: 1; + min-width: 180px; + max-width: 320px; +} + +.filter-bar-search svg { + position: absolute; + left: 10px; + top: 50%; + transform: translateY(-50%); + color: var(--text-muted); + pointer-events: none; +} + +.filter-bar-search input { + width: 100%; + padding: 7px 12px 7px 32px; + background: var(--bg-canvas); + border: 1px solid var(--border); + border-radius: var(--radius); + font-family: 'Inter', sans-serif; + font-size: 13px; + color: var(--text-primary); + outline: none; + transition: border-color 0.15s; +} + +.filter-bar-search input::placeholder { color: var(--text-muted); } +.filter-bar-search input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-ring); } + +.filter-chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 12px; + background: var(--bg-overlay); + border: 1px solid var(--border); + border-radius: var(--radius); + font-size: 12.5px; + font-weight: 450; + color: var(--text-muted); + cursor: pointer; + transition: all 0.12s; + font-family: 'Inter', sans-serif; +} + +.filter-chip:hover { background: var(--bg-muted); color: var(--text-secondary); } +.filter-chip.active { background: var(--accent-glow); border-color: rgba(13,148,136,0.3); color: var(--accent); } + +/* ══════════════════════════════════════════════════ + LOG ENTRY +══════════════════════════════════════════════════ */ +.log-list { + display: flex; + flex-direction: column; +} + +.log-entry { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 10px 20px; + border-bottom: 1px solid var(--border-subtle); + transition: background 0.1s; + font-size: 13px; + cursor: pointer; +} + +.log-entry:last-child { border-bottom: none; } +.log-entry:hover { background: var(--bg-overlay); } + +.log-time { + font-family: 'JetBrains Mono', monospace; + font-size: 11.5px; + color: var(--text-muted); + white-space: nowrap; + padding-top: 2px; + min-width: 152px; + flex-shrink: 0; +} + +.log-msg { + flex: 1; + line-height: 1.5; + color: var(--text-secondary); + word-break: break-word; +} + +.log-msg strong { color: var(--text-primary); font-weight: 500; } + +.log-duration { + font-family: 'JetBrains Mono', monospace; + font-size: 11.5px; + color: var(--text-muted); + white-space: nowrap; + flex-shrink: 0; + padding-top: 2px; + min-width: 54px; + text-align: right; +} + +.log-duration.slow { color: var(--amber); } +.log-duration.error { color: var(--red); } + +/* ══════════════════════════════════════════════════ + SETTINGS LAYOUT +══════════════════════════════════════════════════ */ +.settings-layout { + display: grid; + grid-template-columns: 200px 1fr; + gap: 32px; + align-items: flex-start; +} + +.settings-nav { + position: sticky; + top: 80px; + display: flex; + flex-direction: column; + gap: 2px; +} + +.settings-nav-item { + display: flex; + align-items: center; + gap: 9px; + padding: 8px 12px; + border-radius: var(--radius); + font-size: 13.5px; + color: var(--text-muted); + text-decoration: none; + transition: all 0.1s; + cursor: pointer; + border: none; + background: none; + font-family: 'Inter', sans-serif; + width: 100%; + text-align: left; +} + +.settings-nav-item svg { flex-shrink: 0; opacity: 0.7; } +.settings-nav-item:hover { background: var(--bg-overlay); color: var(--text-secondary); } +.settings-nav-item.active { background: var(--bg-overlay); color: var(--text-primary); font-weight: 500; } +.settings-nav-item.danger { color: var(--red); opacity: 0.8; } +.settings-nav-item.danger:hover { background: var(--red-bg); opacity: 1; } + +.settings-nav-divider { height: 1px; background: var(--border-subtle); margin: 6px 4px; } + +/* ── Form elements (for settings forms) ── */ +.field-group { display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px; } +.field-label { + font-size: 12.5px; + font-weight: 500; + color: var(--text-secondary); + display: flex; + align-items: center; + gap: 6px; +} +.field-hint { font-size: 12px; color: var(--text-muted); line-height: 1.55; } +.field-input, +.field-select, +.field-textarea { + background: var(--bg-canvas); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 8px 12px; + font-family: 'Inter', sans-serif; + font-size: 13.5px; + color: var(--text-primary); + width: 100%; + outline: none; + transition: border-color 0.15s, box-shadow 0.15s; +} +.field-input::placeholder, .field-textarea::placeholder { color: var(--text-muted); } +.field-input:focus, .field-select:focus, .field-textarea:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-ring); +} +.field-select { + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236e7681' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 11px center; + padding-right: 32px; + cursor: pointer; +} +.field-textarea { resize: vertical; min-height: 80px; line-height: 1.6; } +.field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } + +/* ── Section divider ── */ +.settings-section-divider { + height: 1px; + background: var(--border); + margin: 24px 0; +} + +/* ── API key display ── */ +.key-reveal { + display: flex; + align-items: center; + gap: 8px; + background: var(--bg-canvas); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 8px 12px; + font-family: 'JetBrains Mono', monospace; + font-size: 13px; + color: var(--text-secondary); + letter-spacing: 0.2px; +} + +.key-reveal .key-mask { flex: 1; letter-spacing: 2px; color: var(--text-muted); } +.key-reveal .key-copy { flex-shrink: 0; cursor: pointer; color: var(--text-muted); } +.key-reveal .key-copy:hover { color: var(--accent); } + +/* ══════════════════════════════════════════════════ + EMPTY STATE +══════════════════════════════════════════════════ */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 24px; + text-align: center; +} + +.empty-state-icon { + width: 44px; + height: 44px; + background: var(--bg-overlay); + border: 1px solid var(--border); + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-muted); + margin-bottom: 14px; +} + +.empty-state-title { + font-size: 15px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 6px; +} + +.empty-state-text { + font-size: 13.5px; + color: var(--text-muted); + max-width: 320px; + line-height: 1.6; + margin-bottom: 20px; +} + +/* ══════════════════════════════════════════════════ + MODAL OVERLAY +══════════════════════════════════════════════════ */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.65); + z-index: 500; + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + display: none; +} + +.modal-overlay.open { display: flex; } + +.modal { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: 12px; + width: 100%; + max-width: 480px; + box-shadow: 0 8px 40px rgba(0,0,0,0.6); + overflow: hidden; +} + +.modal-header { + padding: 18px 20px 14px; + border-bottom: 1px solid var(--border-subtle); + display: flex; + align-items: center; + gap: 12px; +} + +.modal-title { + font-size: 15px; + font-weight: 600; + color: var(--text-primary); + flex: 1; +} + +.modal-close { + width: 26px; + height: 26px; + display: flex; + align-items: center; + justify-content: center; + border: none; + background: none; + color: var(--text-muted); + cursor: pointer; + border-radius: var(--radius); + transition: all 0.1s; +} +.modal-close:hover { background: var(--bg-overlay); color: var(--text-primary); } + +.modal-body { padding: 20px; } +.modal-footer { padding: 14px 20px; border-top: 1px solid var(--border-subtle); display: flex; justify-content: flex-end; gap: 8px; } + +/* ══════════════════════════════════════════════════ + RESPONSIVE +══════════════════════════════════════════════════ */ +@media (max-width: 960px) { + .stats-grid { grid-template-columns: repeat(2, 1fr); } +} + +@media (max-width: 720px) { + .page-header { flex-direction: column; gap: 12px; } + .page-header-actions { width: 100%; } + .settings-layout { grid-template-columns: 1fr; } + .settings-nav { position: static; flex-direction: row; flex-wrap: wrap; } + .filter-bar { flex-direction: column; align-items: stretch; } + .filter-bar-search { max-width: none; } + .stats-grid { grid-template-columns: 1fr 1fr; } + .field-row { grid-template-columns: 1fr; } +} + +@media (max-width: 480px) { + .stats-grid { grid-template-columns: 1fr; } +} + +/* ── Language switcher ── */ +.lang-switcher { + display: flex; + gap: 8px; +} + +.lang-btn { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 18px; + background: var(--bg-canvas); + border: 1.5px solid var(--border); + border-radius: 8px; + font-family: 'Inter', sans-serif; + font-size: 13px; + font-weight: 500; + color: var(--text-secondary); + cursor: pointer; + transition: border-color 0.15s, color 0.15s, background 0.15s; +} + +.lang-btn:hover { + border-color: var(--accent); + color: var(--text-primary); +} + +.lang-btn.active { + border-color: var(--accent); + background: var(--accent-glow); + color: #fff; + font-weight: 600; +} + +.lang-flag { + font-size: 16px; + line-height: 1; +} + + +/* ═══════════════════════════════════════════════════ + Workspace switcher +═══════════════════════════════════════════════════ */ +.ws-switcher { + position: relative; + display: flex; + align-items: center; + margin-left: 4px; +} + +.ws-switcher-trigger { + display: flex; + align-items: center; + gap: 7px; + padding: 4px 8px 4px 4px; + background: none; + border: 1px solid var(--border); + border-radius: 8px; + cursor: pointer; + color: var(--text-primary); + font-size: 13px; + font-weight: 500; + transition: border-color 0.15s, background 0.15s; + max-width: 200px; + white-space: nowrap; +} +.ws-switcher-trigger:hover { border-color: var(--border-muted, #444c56); background: rgba(255,255,255,0.03); } + +.ws-dot { + width: 22px; + height: 22px; + border-radius: 5px; + background: #0d9488; + color: #fff; + font-size: 11px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.ws-current-name { + overflow: hidden; + text-overflow: ellipsis; + max-width: 140px; +} + +.ws-dropdown { + position: absolute; + top: calc(100% + 6px); + left: 0; + min-width: 230px; + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: 10px; + box-shadow: 0 8px 28px rgba(0,0,0,0.45); + z-index: 200; + overflow: hidden; +} + +.ws-dropdown-label { + padding: 10px 14px 4px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-muted); +} + +.ws-dropdown-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 14px; + cursor: pointer; + transition: background 0.1s; +} +.ws-dropdown-item:hover { background: rgba(255,255,255,0.04); } +.ws-dropdown-item.active { background: rgba(56,139,253,0.08); } + +.ws-item-dot { + width: 30px; + height: 30px; + border-radius: 7px; + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; + font-weight: 700; + color: #fff; + flex-shrink: 0; +} + +.ws-item-info { flex: 1; min-width: 0; } +.ws-item-name { + font-size: 13px; + font-weight: 500; + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.ws-item-role { font-size: 11px; color: var(--text-muted); } + +.ws-dropdown-footer { border-top: 1px solid var(--border-subtle); padding: 6px 8px; } +.ws-dropdown-create { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + padding: 6px 8px; + background: none; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 13px; + color: var(--text-secondary); + transition: background 0.1s, color 0.1s; +} +.ws-dropdown-create:hover { background: rgba(255,255,255,0.04); color: var(--text-primary); } + + +/* ═══════════════════════════════════════════════════ + Agents page — info callout +═══════════════════════════════════════════════════ */ +.agents-info-callout { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 16px; + background: rgba(56,139,253,0.07); + border: 1px solid rgba(56,139,253,0.2); + border-radius: 8px; + font-size: 13px; + color: var(--text-secondary); + line-height: 1.5; + margin-bottom: 24px; +} +.agents-info-callout strong { color: var(--text-primary); } + + +/* ═══════════════════════════════════════════════════ + Agent cards grid +═══════════════════════════════════════════════════ */ +.agents-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); + gap: 16px; + margin-bottom: 40px; +} + +.agent-card { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: 12px; + padding: 20px; + display: flex; + flex-direction: column; + gap: 14px; + transition: border-color 0.15s; +} +.agent-card:hover { border-color: var(--border-muted, #444c56); } + +.agent-card-header { + display: flex; + align-items: flex-start; + gap: 12px; +} + +.agent-card-icon { + width: 38px; + height: 38px; + border-radius: 9px; + background: rgba(56,139,253,0.1); + border: 1px solid rgba(56,139,253,0.2); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: var(--accent); +} + +.agent-card-title-wrap { flex: 1; min-width: 0; } +.agent-card-name { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + line-height: 1.3; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.agent-card-slug { + font-family: var(--font-mono, monospace); + font-size: 11px; + color: var(--text-muted); + margin-top: 2px; +} + +.agent-status-badge { + font-size: 11px; + font-weight: 600; + padding: 2px 8px; + border-radius: 20px; + white-space: nowrap; + flex-shrink: 0; +} +.agent-status-active { background: rgba(63,185,80,0.15); color: #3fb950; border: 1px solid rgba(63,185,80,0.3); } +.agent-status-draft { background: rgba(139,148,158,0.15); color: #8b949e; border: 1px solid rgba(139,148,158,0.3); } +.agent-status-inactive{ background: rgba(248,81,73,0.1); color: #f85149; border: 1px solid rgba(248,81,73,0.2); } + +.agent-card-desc { + font-size: 13px; + color: var(--text-secondary); + line-height: 1.55; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.agent-card-stats { + display: flex; + gap: 16px; + flex-wrap: wrap; +} +.agent-stat { + display: flex; + align-items: center; + gap: 5px; + font-size: 12px; + color: var(--text-muted); +} +.agent-stat-value { font-weight: 600; color: var(--text-secondary); } + +.agent-endpoint-row { + display: flex; + align-items: center; + gap: 8px; + padding: 7px 10px; + background: var(--bg-canvas); + border: 1px solid var(--border-subtle); + border-radius: 6px; +} +.agent-endpoint-url { + flex: 1; + font-family: var(--font-mono, monospace); + font-size: 11px; + color: var(--text-muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.agent-endpoint-copy { + background: none; + border: none; + padding: 2px 4px; + cursor: pointer; + color: var(--text-muted); + border-radius: 4px; + display: flex; + align-items: center; + transition: color 0.1s, background 0.1s; + flex-shrink: 0; +} +.agent-endpoint-copy:hover { color: var(--accent); background: rgba(56,139,253,0.1); } + +.agent-card-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 12px; + border-top: 1px solid var(--border-subtle); +} +.agent-card-date { font-size: 11px; color: var(--text-muted); } + +.agent-card-actions { display: flex; gap: 6px; } +.agent-action-btn { + display: flex; + align-items: center; + gap: 5px; + padding: 4px 10px; + background: none; + border: 1px solid var(--border); + border-radius: 6px; + font-size: 12px; + font-weight: 500; + color: var(--text-secondary); + cursor: pointer; + transition: border-color 0.15s, color 0.15s, background 0.15s; +} +.agent-action-btn:hover { border-color: var(--border-muted, #444c56); color: var(--text-primary); background: rgba(255,255,255,0.03); } +.agent-action-btn.danger:hover { border-color: var(--error, #f87171); color: var(--error, #f87171); background: rgba(248,113,113,0.06); } + + +/* ═══════════════════════════════════════════════════ + Drawer (side panel) +═══════════════════════════════════════════════════ */ +.drawer-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.45); + z-index: 150; + backdrop-filter: blur(2px); +} + +.drawer { + position: fixed; + top: 0; + right: 0; + bottom: 0; + width: 520px; + max-width: 100vw; + background: var(--bg-surface); + border-left: 1px solid var(--border); + z-index: 151; + display: flex; + flex-direction: column; + transform: translateX(100%); + transition: transform 0.22s cubic-bezier(0.4, 0, 0.2, 1); +} +.drawer.open { transform: translateX(0); } + +.drawer-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + padding: 24px 24px 20px; + border-bottom: 1px solid var(--border-subtle); + flex-shrink: 0; +} +.drawer-title { font-size: 16px; font-weight: 600; color: var(--text-primary); } +.drawer-subtitle{ font-size: 13px; color: var(--text-muted); margin-top: 3px; } +.drawer-close { + background: none; + border: 1px solid var(--border); + border-radius: 6px; + padding: 6px; + cursor: pointer; + color: var(--text-muted); + display: flex; + align-items: center; + transition: border-color 0.15s, color 0.15s; + flex-shrink: 0; +} +.drawer-close:hover { border-color: var(--error, #f87171); color: var(--error, #f87171); } + +.drawer-body { + flex: 1; + overflow-y: auto; + padding: 24px; + display: flex; + flex-direction: column; + gap: 28px; +} + +.drawer-section {} +.drawer-section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 4px; +} +.drawer-section-title { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 14px; +} +.drawer-section-header .drawer-section-title { margin-bottom: 0; } +.drawer-section-count { + font-size: 12px; + font-weight: 500; + color: var(--accent); + background: rgba(56,139,253,0.1); + padding: 2px 8px; + border-radius: 20px; +} +.drawer-section-sub { + font-size: 12px; + color: var(--text-muted); + margin-bottom: 12px; + line-height: 1.5; +} + +.drawer-footer { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + padding: 16px 24px; + border-top: 1px solid var(--border-subtle); + flex-shrink: 0; + background: var(--bg-surface); +} + + +/* ═══════════════════════════════════════════════════ + Agent status toggle (in drawer) +═══════════════════════════════════════════════════ */ +.agent-status-toggle { + display: flex; + gap: 0; + background: var(--bg-canvas); + border: 1px solid var(--border); + border-radius: 8px; + padding: 3px; +} +.agent-status-opt { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 6px 12px; + background: none; + border: none; + border-radius: 6px; + font-size: 13px; + font-weight: 500; + color: var(--text-muted); + cursor: pointer; + transition: background 0.15s, color 0.15s; +} +.agent-status-opt.active { + background: var(--bg-surface); + color: var(--text-primary); + box-shadow: 0 1px 3px rgba(0,0,0,0.25); +} +.status-dot { + width: 7px; + height: 7px; + border-radius: 50%; + flex-shrink: 0; +} + + +/* ═══════════════════════════════════════════════════ + Operations picker (in drawer) +═══════════════════════════════════════════════════ */ +.ops-picker { + border: 1px solid var(--border); + border-radius: 8px; + overflow: hidden; +} + +.ops-picker-search { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-bottom: 1px solid var(--border-subtle); + background: var(--bg-canvas); +} +.ops-picker-search-input { + flex: 1; + background: none; + border: none; + outline: none; + font-size: 13px; + color: var(--text-primary); + font-family: inherit; +} +.ops-picker-search-input::placeholder { color: var(--text-muted); } +.ops-picker-clear { + background: none; + border: none; + padding: 2px; + cursor: pointer; + color: var(--text-muted); + display: flex; + align-items: center; + border-radius: 3px; +} +.ops-picker-clear:hover { color: var(--text-secondary); } + +.ops-picker-list { + max-height: 260px; + overflow-y: auto; +} + +.ops-picker-item { + display: flex; + align-items: center; + gap: 10px; + padding: 9px 12px; + cursor: pointer; + border-bottom: 1px solid var(--border-subtle); + transition: background 0.1s; +} +.ops-picker-item:last-child { border-bottom: none; } +.ops-picker-item:hover { background: rgba(255,255,255,0.03); } +.ops-picker-item.selected { background: rgba(56,139,253,0.07); } + +.ops-picker-check { + width: 16px; + height: 16px; + border: 1.5px solid var(--border); + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: border-color 0.1s, background 0.1s; +} +.ops-picker-item.selected .ops-picker-check { + border-color: var(--accent); + background: rgba(56,139,253,0.12); +} + +.ops-picker-info { flex: 1; min-width: 0; } +.ops-picker-name { + font-size: 13px; + font-weight: 500; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.ops-picker-slug { + font-family: var(--font-mono, monospace); + font-size: 10px; + color: var(--text-muted); + margin-top: 1px; +} + +.ops-picker-status-dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; +} +.ops-status-active { background: #3fb950; } +.ops-status-draft { background: #8b949e; } +.ops-status-error { background: #f85149; } +.ops-status-inactive { background: #8b949e; } + +.ops-picker-empty { + padding: 20px; + text-align: center; + font-size: 13px; + color: var(--text-muted); +} + +.ops-picker-footer { + padding: 8px 12px; + border-top: 1px solid var(--border-subtle); + background: var(--bg-canvas); + font-size: 12px; + color: var(--text-secondary); + display: flex; + align-items: center; +} + + +/* ═══════════════════════════════════════════════════ + Agents — recommendation callout +═══════════════════════════════════════════════════ */ +.agents-rec-callout { + display: flex; + align-items: flex-start; + gap: 8px; + margin-top: 10px; + padding: 10px 12px; + background: rgba(210,153,34,0.08); + border: 1px solid rgba(210,153,34,0.25); + border-radius: 7px; + font-size: 12px; + color: var(--text-secondary); + line-height: 1.5; +} +.agents-rec-callout svg { flex-shrink: 0; color: #d2991f; margin-top: 1px; } +.agents-rec-callout strong { color: var(--text-primary); } + + +/* ═══════════════════════════════════════════════════ + Settings — Members enhanced +═══════════════════════════════════════════════════ */ +.member-row { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 0; + border-bottom: 1px solid var(--border-subtle); +} +.member-row:last-child { border-bottom: none; } + +.member-last-active { + font-size: 12px; + color: var(--text-muted); + white-space: nowrap; + min-width: 80px; + text-align: right; +} + +.member-role-select { + background: var(--bg-canvas); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-secondary); + font-size: 12px; + font-weight: 500; + padding: 4px 8px; + cursor: pointer; + outline: none; + transition: border-color 0.15s; + min-width: 110px; +} +.member-role-select:hover { border-color: var(--accent); } +.member-role-select:focus { border-color: var(--accent); } + +.member-remove-btn { + background: none; + border: 1px solid var(--border); + border-radius: 6px; + padding: 5px 6px; + cursor: pointer; + color: var(--text-muted); + display: flex; + align-items: center; + justify-content: center; + transition: border-color 0.15s, color 0.15s, background 0.15s; + flex-shrink: 0; +} +.member-remove-btn:hover { + border-color: var(--error, #f87171); + color: var(--error, #f87171); + background: rgba(248,113,113,0.07); +} + +.member-role-badge { + font-size: 11px; + font-weight: 600; + padding: 2px 9px; + border-radius: 20px; + white-space: nowrap; + flex-shrink: 0; +} +.role-owner { background: rgba(56,139,253,0.15); color: #58a6ff; border: 1px solid rgba(56,139,253,0.3); } +.role-admin { background: rgba(139,92,246,0.15); color: #a78bfa; border: 1px solid rgba(139,92,246,0.3); } +.role-developer { background: rgba(63,185,80,0.12); color: #3fb950; border: 1px solid rgba(63,185,80,0.25); } +.role-viewer { background: rgba(139,148,158,0.15); color: #8b949e; border: 1px solid rgba(139,148,158,0.3); } + +.role-def-row { + display: flex; + align-items: flex-start; + gap: 16px; + padding: 11px 20px; + border-bottom: 1px solid var(--border-subtle); +} +.role-def-row:last-child { border-bottom: none; } +.role-def-text { + font-size: 13px; + color: var(--text-secondary); + line-height: 1.5; +} + +/* ── Workspace dropdown management links ── */ +.ws-dropdown-divider { + height: 1px; + background: var(--border-subtle); + margin: 4px 0; +} +.ws-dropdown-mgmt-link { + display: flex; + align-items: center; + gap: 8px; + padding: 7px 14px; + font-size: 13px; + color: var(--text-secondary); + text-decoration: none; + transition: background 0.1s, color 0.1s; + cursor: pointer; +} +.ws-dropdown-mgmt-link:hover { + background: rgba(255,255,255,0.04); + color: var(--text-primary); +} +.ws-dropdown-mgmt-link svg { + flex-shrink: 0; + opacity: 0.7; +} diff --git a/apps/ui/css/settings.css b/apps/ui/css/settings.css new file mode 100644 index 0000000..452ddc1 --- /dev/null +++ b/apps/ui/css/settings.css @@ -0,0 +1,144 @@ +.avatar-upload { + display: flex; + align-items: center; + gap: 16px; + padding: 16px; + background: var(--bg-canvas); + border: 1px solid var(--border); + border-radius: var(--radius); + margin-bottom: 4px; +} + +.avatar-large { + width: 52px; + height: 52px; + border-radius: 50%; + background: linear-gradient(135deg, #0d9488, #6366f1); + color: #fff; + font-size: 18px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + border: 2px solid var(--border); +} + +.avatar-upload-actions { flex: 1; } +.avatar-name { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 3px; } +.avatar-sub { font-size: 12px; color: var(--text-muted); margin-bottom: 8px; } + +.save-bar { + position: sticky; + bottom: 0; + background: rgba(13,17,23,0.9); + backdrop-filter: blur(12px); + border-top: 1px solid var(--border); + padding: 12px 20px; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + margin-top: auto; + display: none; +} + +.save-bar.visible { display: flex; } + +.section-anchor { scroll-margin-top: 80px; } + +.danger-zone-action { + display: flex; + align-items: flex-start; + gap: 16px; + padding: 16px; + border-bottom: 1px solid var(--border-subtle); +} + +.danger-zone-action:last-child { border-bottom: none; } + +.danger-zone-text { flex: 1; } +.danger-zone-title { font-size: 13.5px; font-weight: 500; color: var(--text-primary); margin-bottom: 3px; } +.danger-zone-desc { font-size: 12.5px; color: var(--text-muted); line-height: 1.5; } + +.toggle-setting-row { + display: flex; + align-items: center; + gap: 14px; + padding: 14px 0; + border-bottom: 1px solid var(--border-subtle); +} + +.toggle-setting-row:last-child { border-bottom: none; } + +.toggle-sm { + width: 36px; + height: 20px; + border-radius: 10px; + background: var(--bg-muted); + border: 1px solid var(--border); + position: relative; + flex-shrink: 0; + cursor: pointer; + transition: background 0.2s; +} + +.toggle-sm.on { background: var(--accent); border-color: var(--accent-dim); } + +.toggle-sm::after { + content: ''; + position: absolute; + left: 2px; top: 1px; + width: 16px; height: 16px; + border-radius: 50%; + background: #fff; + box-shadow: 0 1px 3px rgba(0,0,0,0.3); + transition: transform 0.2s; +} + +.toggle-sm.on::after { transform: translateX(16px); } + +.toggle-setting-text { flex: 1; } +.toggle-setting-label { font-size: 13.5px; font-weight: 500; color: var(--text-primary); } +.toggle-setting-desc { font-size: 12px; color: var(--text-muted); margin-top: 2px; } + +.member-row { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 0; + border-bottom: 1px solid var(--border-subtle); +} + +.member-row:last-child { border-bottom: none; } + +.member-avatar { + width: 30px; + height: 30px; + border-radius: 50%; + background: var(--bg-muted); + border: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 700; + color: var(--text-secondary); + flex-shrink: 0; +} + +.member-info { flex: 1; } +.member-name { font-size: 13.5px; color: var(--text-primary); font-weight: 500; } +.member-email { font-size: 12px; color: var(--text-muted); } + +.member-role-badge { + font-size: 11px; + font-weight: 600; + padding: 2px 8px; + border-radius: 4px; + border: 1px solid; +} + +.role-admin { background: rgba(13,148,136,0.1); border-color: rgba(13,148,136,0.25); color: var(--accent); } +.role-member { background: var(--bg-overlay); border-color: var(--border); color: var(--text-muted); } +.role-viewer { background: rgba(88,166,255,0.07); border-color: var(--blue-border); color: var(--blue); } diff --git a/apps/ui/css/usage.css b/apps/ui/css/usage.css new file mode 100644 index 0000000..9785ebe --- /dev/null +++ b/apps/ui/css/usage.css @@ -0,0 +1,122 @@ +.period-select { + background: var(--bg-canvas); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 7px 28px 7px 11px; + font-family: 'Inter', sans-serif; + font-size: 13px; + color: var(--text-secondary); + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12'%3E%3Cpath fill='%236e7681' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 8px center; + cursor: pointer; + outline: none; + transition: border-color 0.15s; +} + +.period-select:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-ring); +} + +.chart-placeholder { + background: var(--bg-canvas); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + height: 200px; + display: flex; + align-items: flex-end; + padding: 12px 16px; + gap: 4px; + overflow: hidden; + margin-bottom: 24px; + position: relative; +} + +.chart-bar { + flex: 1; + border-radius: 3px 3px 0 0; + min-width: 0; + transition: opacity 0.15s; + position: relative; + cursor: pointer; +} + +.chart-bar:hover { opacity: 0.8; } +.chart-bar:hover::after { + content: attr(data-tip); + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: 5px; + padding: 4px 8px; + font-size: 11px; + color: var(--text-primary); + white-space: nowrap; + pointer-events: none; + margin-bottom: 4px; +} + +.chart-bar.success { background: linear-gradient(180deg, var(--accent) 0%, rgba(13,148,136,0.4) 100%); } +.chart-bar.error { background: linear-gradient(180deg, var(--red) 0%, rgba(248,81,73,0.3) 100%); } + +.chart-label { + position: absolute; + bottom: -20px; + left: 0; + right: 0; + text-align: center; + font-size: 10px; + color: var(--text-muted); +} + +.chart-wrap { + position: relative; + padding-bottom: 24px; +} + +.chart-y-grid { + position: absolute; + left: 0; right: 0; top: 0; bottom: 24px; + display: flex; + flex-direction: column; + justify-content: space-between; + pointer-events: none; +} + +.chart-y-line { + width: 100%; + height: 1px; + background: var(--border-subtle); +} + +.quota-bar-wrap { + height: 8px; + background: var(--bg-muted); + border-radius: 4px; + overflow: hidden; + margin: 8px 0 4px; +} + +.quota-bar-fill { + height: 100%; + border-radius: 4px; + background: linear-gradient(90deg, var(--accent), #7c3aed); +} + +.latency-bar { + display: flex; + height: 12px; + border-radius: 3px; + overflow: hidden; + gap: 2px; +} + +.latency-seg { border-radius: 2px; } +.latency-p50 { background: var(--accent); } +.latency-p95 { background: var(--amber); } +.latency-p99 { background: var(--red); } diff --git a/apps/ui/css/variables.css b/apps/ui/css/variables.css new file mode 100644 index 0000000..b48a1a7 --- /dev/null +++ b/apps/ui/css/variables.css @@ -0,0 +1,64 @@ +:root { + --bg-canvas: #0d1117; + --bg-surface: #161b22; + --bg-overlay: #21262d; + --bg-muted: #30363d; + --border: rgba(48, 54, 61, 0.9); + --border-subtle: rgba(33, 38, 45, 0.9); + + --text-primary: #e6edf3; + --text-secondary: #8b949e; + --text-muted: #6e7681; + + --accent: #0d9488; + --accent-dim: #0f766e; + --accent-bright: #14b8a6; + --accent-glow: rgba(13, 148, 136, 0.15); + --accent-ring: rgba(13, 148, 136, 0.25); + + --red: #f85149; + --red-bg: rgba(248, 81, 73, 0.10); + --red-border: rgba(248, 81, 73, 0.25); + --amber: #d29922; + --amber-bg: rgba(210, 153, 34, 0.10); + --amber-border: rgba(210, 153, 34, 0.25); + --green: #3fb950; + --green-bg: rgba(63, 185, 80, 0.10); + --green-border: rgba(63, 185, 80, 0.25); + --blue: #58a6ff; + --blue-bg: rgba(88, 166, 255, 0.08); + --blue-border: rgba(88, 166, 255, 0.20); + --purple: #bc8cff; + --purple-bg: rgba(188, 140, 255, 0.08); + --purple-border: rgba(188, 140, 255, 0.20); + + --radius-sm: 5px; + --radius: 7px; + --radius-lg: 10px; + + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow: 0 1px 3px rgba(0, 0, 0, 0.4), 0 4px 16px rgba(0, 0, 0, 0.3); +} + +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +html, body { + height: 100%; + min-width: 360px; + font-family: 'Inter', system-ui, sans-serif; + background: var(--bg-canvas); + color: var(--text-primary); + font-size: 14px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { color: inherit; text-decoration: none; } +button { cursor: pointer; font-family: inherit; } + +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: var(--bg-muted); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: #484f58; } +::selection { background: var(--accent-glow); color: var(--text-primary); } diff --git a/apps/ui/css/wizard.css b/apps/ui/css/wizard.css new file mode 100644 index 0000000..0cfa730 --- /dev/null +++ b/apps/ui/css/wizard.css @@ -0,0 +1,1884 @@ +/* ══════════════════════════════════════════════════ + WIZARD — shell +══════════════════════════════════════════════════ */ +.wizard-shell { + min-height: calc(100vh - 60px); + display: flex; + flex-direction: column; +} + +/* ══════════════════════════════════════════════════ + TOP PROGRESS STRIP +══════════════════════════════════════════════════ */ +.progress-strip { + background: var(--bg-surface); + border-bottom: 1px solid var(--border); + padding: 0 40px; + display: flex; + align-items: center; + height: 56px; + gap: 20px; + position: sticky; + top: 60px; + z-index: 30; + box-shadow: 0 1px 0 var(--border-subtle), 0 4px 16px rgba(0, 0, 0, 0.4); +} + +.back-to-catalog-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border-radius: 6px; + background: none; + border: 1px solid var(--border); + color: var(--text-secondary); + font-size: 13px; + font-weight: 500; + cursor: pointer; + font-family: 'Inter', sans-serif; + transition: all 0.15s; + white-space: nowrap; + text-decoration: none; +} + +.back-to-catalog-btn:hover { + background: var(--bg-overlay); + color: var(--text-primary); + border-color: var(--bg-muted); +} + +.progress-divider { + width: 1px; + height: 20px; + background: var(--border); + margin: 0 4px; +} + +.progress-label { + font-size: 13px; + color: var(--text-secondary); + white-space: nowrap; +} + +.progress-bar-wrap { + flex: 1; + height: 4px; + background: var(--bg-muted); + border-radius: 10px; + overflow: hidden; + min-width: 80px; +} + +.progress-bar-fill { + height: 100%; + width: 20%; + background: linear-gradient(90deg, var(--accent), #7c3aed); + border-radius: 10px; + transition: width 0.4s ease; + position: relative; +} + +.progress-bar-fill::after { + content: ''; + position: absolute; + right: -1px; + top: 50%; + transform: translateY(-50%); + width: 8px; + height: 8px; + border-radius: 50%; + background: #7c3aed; + box-shadow: 0 0 0 2px var(--bg-surface), 0 0 8px rgba(124, 58, 237, 0.6); +} + +.progress-pct { + font-size: 12px; + font-weight: 600; + color: var(--accent); + white-space: nowrap; + min-width: 34px; + text-align: right; + font-variant-numeric: tabular-nums; +} + +.progress-close { + margin-left: 4px; + width: 28px; + height: 28px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + background: none; + border: 1px solid var(--border); + color: var(--text-muted); + cursor: pointer; + transition: all 0.15s; +} + +.progress-close:hover { + background: var(--bg-overlay); + color: var(--text-primary); + border-color: var(--bg-muted); +} + +/* ══════════════════════════════════════════════════ + WIZARD BODY +══════════════════════════════════════════════════ */ +.wizard-body { + flex: 1; + display: flex; + max-width: 1100px; + width: 100%; + margin: 0 auto; + padding: 40px 40px 140px; + gap: 36px; + align-items: flex-start; +} + +/* ══════════════════════════════════════════════════ + STEP SIDEBAR +══════════════════════════════════════════════════ */ +.step-sidebar { + width: 248px; + flex-shrink: 0; + position: sticky; + top: 136px; +} + +.step-sidebar-card { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: 12px; + overflow: hidden; +} + +.step-sidebar-header { + padding: 16px 18px 14px; + border-bottom: 1px solid var(--border-subtle); + display: flex; + align-items: center; + gap: 10px; +} + +.step-sidebar-logo-mark { + width: 24px; + height: 24px; + border-radius: 6px; + background: linear-gradient(135deg, var(--accent), #7c3aed); + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 11px; + font-weight: 800; + flex-shrink: 0; +} + +.step-sidebar-brand { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + letter-spacing: -0.2px; +} + +.step-sidebar-brand span { + color: var(--text-muted); + font-weight: 400; +} + +.step-sidebar-body { + padding: 18px 14px 14px; +} + +.step-sidebar-section-label { + font-size: 10px; + font-weight: 600; + letter-spacing: 0.8px; + text-transform: uppercase; + color: var(--text-muted); + padding: 0 4px; + margin-bottom: 14px; +} + +/* Step list */ +.steps-list { + display: flex; + flex-direction: column; + position: relative; +} + +.steps-list::before { + content: ''; + position: absolute; + left: 17px; + top: 32px; + bottom: 32px; + width: 1px; + background: var(--border); + z-index: 0; +} + +.step-item { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 9px 10px; + border-radius: 8px; + cursor: pointer; + position: relative; + z-index: 1; + transition: background 0.15s; + border: 1px solid transparent; +} + +.step-item:hover { background: var(--bg-overlay); } + +.step-item.active { + background: var(--accent-glow); + border-color: rgba(13, 148, 136, 0.2); +} + +.step-item.pending:hover { background: rgba(33, 38, 45, 0.6); } + +/* Step indicator circle */ +.step-indicator { + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + font-size: 12px; + font-weight: 700; + border: 2px solid var(--step-pending); + background: var(--bg-overlay); + color: var(--text-muted); + transition: all 0.2s; + position: relative; + z-index: 2; +} + +.step-item.done .step-indicator { + background: var(--step-done); + border-color: var(--step-done); + color: var(--bg-canvas); + box-shadow: 0 0 0 3px rgba(63, 185, 80, 0.15); +} + +.step-item.active .step-indicator { + background: var(--bg-surface); + border-color: var(--step-active); + color: var(--accent); + box-shadow: 0 0 0 4px var(--accent-ring); + font-weight: 700; +} + +.step-item.pending .step-indicator { + background: var(--bg-overlay); + border-color: var(--step-pending); + color: var(--text-muted); +} + +/* Step text content */ +.step-content { + flex: 1; + min-width: 0; + padding-top: 3px; +} + +.step-number { + font-size: 10.5px; + color: var(--text-muted); + font-weight: 500; + margin-bottom: 1px; + letter-spacing: 0.2px; +} + +.step-name { + font-size: 13.5px; + font-weight: 500; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.step-item.done .step-name { color: var(--text-primary); } +.step-item.active .step-name { color: var(--accent-bright); font-weight: 600; } +.step-item.pending .step-name { color: var(--text-muted); } + +.step-status-text { + font-size: 11px; + margin-top: 2px; +} + +.step-item.done .step-status-text { color: var(--step-done); } +.step-item.active .step-status-text { color: var(--accent); } +.step-item.pending .step-status-text { color: var(--text-muted); } + +/* Sidebar footer help card */ +.sidebar-help { + margin-top: 4px; + background: var(--bg-overlay); + border: 1px solid var(--border-subtle); + border-radius: 8px; + padding: 14px 16px; +} + +.sidebar-help-title { + font-size: 12.5px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 6px; + display: flex; + align-items: center; + gap: 6px; +} + +.sidebar-help-title svg { opacity: 0.7; flex-shrink: 0; } + +.sidebar-help-text { + font-size: 12px; + color: var(--text-secondary); + line-height: 1.6; +} + +.sidebar-help-link { + color: var(--accent); + text-decoration: none; + font-weight: 500; +} + +.sidebar-help-link:hover { text-decoration: underline; } + +/* ══════════════════════════════════════════════════ + MAIN STEP PANEL +══════════════════════════════════════════════════ */ +.step-panel { + flex: 1; + min-width: 0; +} + +.step-panel-header { + margin-bottom: 28px; +} + +.step-panel-eyebrow { + display: flex; + align-items: center; + gap: 8px; + font-size: 11.5px; + font-weight: 600; + letter-spacing: 0.6px; + text-transform: uppercase; + color: var(--accent); + margin-bottom: 10px; +} + +.step-panel-eyebrow-line { + width: 20px; + height: 2px; + background: var(--accent); + border-radius: 1px; + flex-shrink: 0; +} + +.step-panel-title { + font-size: 26px; + font-weight: 700; + letter-spacing: -0.6px; + color: var(--text-primary); + margin-bottom: 8px; + line-height: 1.2; +} + +.step-panel-subtitle { + font-size: 14px; + color: var(--text-secondary); + line-height: 1.65; + max-width: 580px; +} + +/* ── Protocol cards ── */ +.protocol-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + margin-bottom: 24px; +} + +.protocol-card { + background: var(--bg-surface); + border: 1.5px solid var(--border); + border-radius: 10px; + padding: 20px 18px 18px; + cursor: pointer; + transition: all 0.18s ease; + position: relative; + overflow: hidden; +} + +.protocol-card:hover { + border-color: var(--accent); + background: rgba(13, 148, 136, 0.06); + transform: translateY(-1px); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(13, 148, 136, 0.15); +} + +.protocol-card.selected { + border-color: var(--accent); + background: var(--accent-glow); + box-shadow: + 0 0 0 1px var(--accent), + 0 4px 24px rgba(0, 0, 0, 0.5), + 0 0 20px rgba(13, 148, 136, 0.12); +} + +.protocol-card-check { + position: absolute; + top: 12px; + right: 12px; + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--accent); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transform: scale(0.6); + transition: all 0.18s ease; + flex-shrink: 0; +} + +.protocol-card.selected .protocol-card-check { + opacity: 1; + transform: scale(1); +} + +.protocol-card-check svg { + width: 10px; + height: 10px; + stroke: #fff; + stroke-width: 2.5; + fill: none; +} + +.protocol-icon-wrap { + width: 44px; + height: 44px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 14px; + flex-shrink: 0; + transition: all 0.18s; +} + +.protocol-card.rest .protocol-icon-wrap { + background: rgba(88, 166, 255, 0.12); + border: 1px solid rgba(88, 166, 255, 0.2); +} + +.protocol-card.graphql .protocol-icon-wrap { + background: rgba(188, 140, 255, 0.12); + border: 1px solid rgba(188, 140, 255, 0.2); +} + +.protocol-card.grpc .protocol-icon-wrap { + background: rgba(13, 148, 136, 0.12); + border: 1px solid rgba(13, 148, 136, 0.2); +} + +.protocol-card.selected.rest .protocol-icon-wrap { + background: rgba(88, 166, 255, 0.18); + border-color: rgba(88, 166, 255, 0.35); +} + +.protocol-card.selected.graphql .protocol-icon-wrap { + background: rgba(188, 140, 255, 0.18); + border-color: rgba(188, 140, 255, 0.35); +} + +.protocol-card.selected.grpc .protocol-icon-wrap { + background: rgba(13, 148, 136, 0.2); + border-color: rgba(13, 148, 136, 0.4); +} + +.protocol-card-name { + font-size: 15px; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 4px; + letter-spacing: -0.2px; +} + +.protocol-card.selected .protocol-card-name { color: #fff; } + +.protocol-card-tagline { + font-size: 12px; + color: var(--text-secondary); + margin-bottom: 14px; + line-height: 1.5; +} + +.protocol-card.selected .protocol-card-tagline { color: var(--accent-bright); } + +.protocol-card-tags { + display: flex; + flex-wrap: wrap; + gap: 5px; +} + +.protocol-tag { + font-size: 10.5px; + font-weight: 500; + padding: 2px 8px; + border-radius: 4px; + border: 1px solid var(--border); + background: var(--bg-overlay); + color: var(--text-muted); + letter-spacing: 0.1px; + transition: all 0.15s; +} + +.protocol-card.selected .protocol-tag { + background: rgba(13, 148, 136, 0.12); + border-color: rgba(13, 148, 136, 0.3); + color: var(--accent-bright); +} + +.protocol-card.rest.selected .protocol-tag { + background: rgba(88, 166, 255, 0.1); + border-color: rgba(88, 166, 255, 0.25); + color: #58a6ff; +} + +.protocol-card.graphql.selected .protocol-tag { + background: rgba(188, 140, 255, 0.1); + border-color: rgba(188, 140, 255, 0.25); + color: #bc8cff; +} + +/* ── REST method picker ── */ +.method-section { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: 10px; + overflow: hidden; + margin-bottom: 20px; +} + +.method-section-header { + padding: 14px 20px; + border-bottom: 1px solid var(--border-subtle); + display: flex; + align-items: center; + gap: 10px; +} + +.method-section-title { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); +} + +.method-section-subtitle { + font-size: 12px; + color: var(--text-muted); + margin-left: auto; +} + +.method-section-body { + padding: 18px 20px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.method-grid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 8px; +} + +.method-btn { + padding: 10px 8px; + border-radius: 7px; + border: 1.5px solid var(--border); + background: var(--bg-overlay); + cursor: pointer; + transition: all 0.15s; + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + font-family: 'Inter', sans-serif; +} + +.method-btn:hover { border-color: var(--bg-muted); background: rgba(33, 38, 45, 0.8); } + +.method-btn.selected-get { border-color: #3fb950; background: rgba(63, 185, 80, 0.08); } +.method-btn.selected-post { border-color: #58a6ff; background: rgba(88, 166, 255, 0.08); } +.method-btn.selected-put { border-color: #d29922; background: rgba(210, 153, 34, 0.08); } +.method-btn.selected-patch { border-color: #bc8cff; background: rgba(188, 140, 255, 0.08); } +.method-btn.selected-delete { border-color: #f85149; background: rgba(248, 81, 73, 0.08); } + +.method-verb { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.5px; + font-variant-numeric: tabular-nums; + color: var(--text-muted); +} + +.method-btn.selected-get .method-verb { color: #3fb950; } +.method-btn.selected-post .method-verb { color: #58a6ff; } +.method-btn.selected-put .method-verb { color: #d29922; } +.method-btn.selected-patch .method-verb { color: #bc8cff; } +.method-btn.selected-delete .method-verb { color: #f85149; } + +.method-label { font-size: 10px; color: var(--text-muted); } + +/* ── Info callout ── */ +.info-callout { + display: flex; + gap: 12px; + padding: 14px 16px; + background: rgba(13, 148, 136, 0.07); + border: 1px solid rgba(13, 148, 136, 0.2); + border-radius: 8px; +} + +.info-callout-icon { + width: 18px; + height: 18px; + flex-shrink: 0; + margin-top: 1px; + color: var(--accent); +} + +.info-callout-body { flex: 1; } + +.info-callout-title { + font-size: 12.5px; + font-weight: 600; + color: var(--accent-bright); + margin-bottom: 4px; +} + +.info-callout-text { + font-size: 12px; + color: var(--text-secondary); + line-height: 1.65; +} + +.info-callout-text a { + color: var(--accent); + text-decoration: none; + font-weight: 500; +} + +.info-callout-text a:hover { text-decoration: underline; } + +/* ── Config card ── */ +.config-card { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: 10px; + overflow: hidden; + margin-bottom: 20px; +} + +.config-card-header { + padding: 14px 20px; + border-bottom: 1px solid var(--border-subtle); + display: flex; + align-items: center; + gap: 12px; +} + +.config-card-header-icon { + width: 28px; + height: 28px; + border-radius: 7px; + background: var(--bg-overlay); + border: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; + flex-shrink: 0; +} + +.config-card-title { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); +} + +.config-card-subtitle { + font-size: 12px; + color: var(--text-muted); + margin-top: 1px; +} + +.config-card-optional { + margin-left: auto; + font-size: 10.5px; + font-weight: 500; + color: var(--text-muted); + background: var(--bg-overlay); + border: 1px solid var(--border-subtle); + padding: 2px 8px; + border-radius: 10px; +} + +.config-card-body { + padding: 20px; + display: flex; + flex-direction: column; + gap: 16px; +} + +/* Form fields */ +.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } +.form-group { display: flex; flex-direction: column; gap: 6px; } + +.form-label { + font-size: 12.5px; + font-weight: 500; + color: var(--text-secondary); + display: flex; + align-items: center; + gap: 6px; +} + +.form-label-required { + font-size: 10px; + font-weight: 600; + padding: 1px 5px; + border-radius: 4px; + background: rgba(248, 81, 73, 0.12); + color: var(--red); + border: 1px solid rgba(248, 81, 73, 0.2); + letter-spacing: 0.2px; +} + +.form-label-optional { + font-size: 11px; + color: var(--text-muted); + font-weight: 400; +} + +.form-hint { + font-size: 11.5px; + color: var(--text-muted); + line-height: 1.55; +} + +.form-input, +.form-textarea, +.form-select { + background: var(--bg-canvas); + border: 1px solid var(--border); + border-radius: 6px; + padding: 9px 12px; + font-family: 'Inter', sans-serif; + font-size: 13.5px; + color: var(--text-primary); + width: 100%; + outline: none; + transition: border-color 0.15s, box-shadow 0.15s; + appearance: none; + -webkit-appearance: none; +} + +.form-input::placeholder, +.form-textarea::placeholder { color: var(--text-muted); } + +.form-input:focus, +.form-textarea:focus, +.form-select:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-ring); +} + +.form-select { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236e7681' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 11px center; + padding-right: 32px; + cursor: pointer; +} + +.form-textarea { + resize: vertical; + min-height: 90px; + line-height: 1.6; +} + +/* Toggle switch */ +.toggle-row { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 14px; + border-radius: 8px; + background: var(--bg-overlay); + border: 1px solid var(--border-subtle); + cursor: pointer; + transition: background 0.15s; +} + +.toggle-row:hover { background: rgba(33, 38, 45, 0.8); } + +.toggle { + width: 36px; + height: 20px; + border-radius: 10px; + background: var(--bg-muted); + border: 1px solid var(--border); + position: relative; + flex-shrink: 0; + transition: background 0.2s; + cursor: pointer; +} + +.toggle.on { + background: var(--accent); + border-color: var(--accent-dim); +} + +.toggle::after { + content: ''; + position: absolute; + left: 2px; + top: 1px; + width: 16px; + height: 16px; + border-radius: 50%; + background: #fff; + box-shadow: 0 1px 3px rgba(0,0,0,0.3); + transition: transform 0.2s ease; +} + +.toggle.on::after { transform: translateX(16px); } + +.toggle-text { flex: 1; } +.toggle-label { font-size: 13px; font-weight: 500; color: var(--text-primary); } +.toggle-desc { font-size: 11.5px; color: var(--text-muted); margin-top: 1px; } + +/* ══════════════════════════════════════════════════ + BOTTOM ACTION BAR — frosted dark glass +══════════════════════════════════════════════════ */ +.action-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background: rgba(13, 17, 23, 0.82); + backdrop-filter: blur(16px) saturate(140%); + -webkit-backdrop-filter: blur(16px) saturate(140%); + border-top: 1px solid rgba(48, 54, 61, 0.7); + z-index: 50; + box-shadow: + 0 -1px 0 rgba(255, 255, 255, 0.03), + 0 -8px 32px rgba(0, 0, 0, 0.6); +} + +.action-bar-inner { + max-width: 1100px; + margin: 0 auto; + padding: 0 40px; + height: 70px; + display: flex; + align-items: center; + gap: 12px; +} + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 9px 18px; + border-radius: 7px; + font-family: 'Inter', sans-serif; + font-size: 13.5px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s; + border: none; + white-space: nowrap; +} + +.btn-back { + background: var(--bg-overlay); + color: var(--text-secondary); + border: 1px solid var(--border); +} + +.btn-back:hover { + background: var(--bg-muted); + color: var(--text-primary); + border-color: var(--bg-muted); +} + +.btn-save-draft { + background: none; + color: var(--text-muted); + border: none; + font-size: 13px; + font-weight: 400; + cursor: pointer; + font-family: 'Inter', sans-serif; + padding: 9px 12px; + border-radius: 6px; + transition: all 0.15s; +} + +.btn-save-draft:hover { + color: var(--text-secondary); + background: rgba(33, 38, 45, 0.6); +} + +.action-bar-spacer { flex: 1; } + +.step-counter { + font-size: 12.5px; + color: var(--text-muted); + font-variant-numeric: tabular-nums; +} + +.step-counter strong { + color: var(--text-secondary); + font-weight: 600; +} + +.btn-continue { + background: var(--accent); + color: #fff; + padding: 10px 24px; + font-size: 14px; + border: 1px solid var(--accent-dim); + box-shadow: 0 1px 4px rgba(13, 148, 136, 0.4); + transition: all 0.15s; +} + +.btn-continue:hover { + background: var(--accent-dim); + border-color: transparent; + box-shadow: + 0 2px 10px rgba(13, 148, 136, 0.5), + 0 0 0 3px var(--accent-ring); + transform: translateY(-1px); +} + +.btn-continue:active { + transform: translateY(0); + box-shadow: 0 1px 4px rgba(13, 148, 136, 0.3); +} + +/* ── Code editor ── */ +.code-block { + border: 1px solid var(--border); + border-radius: 8px; + overflow: hidden; +} + +.code-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 14px; + background: #1e2330; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} + +.code-dots { display: flex; gap: 5px; } +.code-dots span { width: 10px; height: 10px; border-radius: 50%; } +.code-dots span:nth-child(1) { background: #ff5f56; } +.code-dots span:nth-child(2) { background: #ffbd2e; } +.code-dots span:nth-child(3) { background: #27c93f; } + +.code-toolbar-label { + font-size: 10.5px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: #8b949e; +} + +.code-textarea { + background: #161b22 !important; + color: #c9d1d9 !important; + font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace !important; + font-size: 12.5px !important; + line-height: 1.65 !important; + border: none !important; + border-radius: 0 !important; + resize: vertical; + min-height: 160px; +} + +.code-textarea:focus { + border-color: transparent !important; + box-shadow: none !important; +} + +/* ── Section divider ── */ +.section-divider { + display: flex; + align-items: center; + gap: 12px; + margin: 8px 0; +} + +.section-divider-label { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.07em; + text-transform: uppercase; + color: var(--text-muted); + white-space: nowrap; +} + +.section-divider-line { + flex: 1; + height: 1px; + background: var(--border); +} + +/* ── Step pane ── */ +.step-pane { display: flex; flex-direction: column; } + +/* ── Monospace input variant ── */ +.input-mono { + font-family: 'JetBrains Mono', 'Fira Code', monospace; + font-size: 13px; + letter-spacing: -0.2px; +} + +/* ── Step-state colours (wizard-specific) ── */ +:root { + --step-done: var(--green); + --step-active: var(--accent); + --step-pending: var(--bg-muted); +} + +/* ══════════════════════════════════════════════ + Upstream selector (Step 3) + ══════════════════════════════════════════════ */ + +/* ── Combobox trigger ── */ +.upstream-combobox { + position: relative; +} + +.upstream-combobox-trigger { + display: flex; + align-items: center; + gap: 8px; + padding: 9px 12px; + background: var(--bg-canvas); + border: 1px solid var(--border); + border-radius: 7px; + cursor: pointer; + transition: border-color 0.15s, box-shadow 0.15s; + user-select: none; +} + +.upstream-combobox-trigger:hover { border-color: var(--accent); } + +.upstream-combobox.open .upstream-combobox-trigger { + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(13, 148, 136, 0.15); + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.upstream-combobox-value { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + gap: 10px; +} + +.upstream-combobox-placeholder { + font-size: 13.5px; + color: var(--text-muted); +} + +.upstream-combobox-chevron { + flex-shrink: 0; + transition: transform 0.2s; +} +.upstream-combobox.open .upstream-combobox-chevron { transform: rotate(180deg); } + +/* Value when upstream is selected (shown inside trigger) */ +.upstream-trigger-name { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + font-family: 'JetBrains Mono', 'Fira Code', monospace; + letter-spacing: -0.2px; +} + +.upstream-trigger-url { + font-size: 11.5px; + color: var(--text-muted); + font-family: 'JetBrains Mono', 'Fira Code', monospace; + letter-spacing: -0.2px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* ── Dropdown panel ── */ +.upstream-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 200; + background: var(--bg-surface); + border: 1px solid var(--accent); + border-top: none; + border-bottom-left-radius: 7px; + border-bottom-right-radius: 7px; + box-shadow: 0 8px 24px rgba(0,0,0,0.35); + overflow: hidden; +} + +/* Search row */ +.upstream-search-wrap { + display: flex; + align-items: center; + gap: 8px; + padding: 9px 12px; + border-bottom: 1px solid var(--border-subtle); +} + +.upstream-search-input { + flex: 1; + background: transparent; + border: none; + outline: none; + font-family: 'Inter', sans-serif; + font-size: 13px; + color: var(--text-primary); +} +.upstream-search-input::placeholder { color: var(--text-muted); } + +/* List of upstream items */ +.upstream-dropdown-list { + max-height: 220px; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: var(--border) transparent; +} + +.upstream-dropdown-item { + display: flex; + align-items: center; + gap: 10px; + padding: 9px 14px; + cursor: pointer; + transition: background 0.12s; +} +.upstream-dropdown-item:hover { background: rgba(13, 148, 136, 0.07); } +.upstream-dropdown-item.selected { background: rgba(13, 148, 136, 0.1); } + +.upstream-dropdown-item-info { flex: 1; min-width: 0; } + +.upstream-dropdown-item-name { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + font-family: 'JetBrains Mono', 'Fira Code', monospace; + letter-spacing: -0.2px; +} + +.upstream-dropdown-item-url { + font-size: 11px; + color: var(--text-muted); + font-family: 'JetBrains Mono', 'Fira Code', monospace; + letter-spacing: -0.2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.upstream-dropdown-item-check { + opacity: 0; + flex-shrink: 0; +} +.upstream-dropdown-item.selected .upstream-dropdown-item-check { opacity: 1; } + +/* Empty state */ +.upstream-dropdown-empty { + padding: 16px 14px; + font-size: 12.5px; + color: var(--text-muted); + text-align: center; +} + +/* Footer: Register new */ +/* ── Register new upstream trigger row ── */ +.upstream-new-trigger { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + border: 1.5px dashed var(--border); + border-radius: 8px; + cursor: pointer; + font-size: 13px; + font-weight: 600; + color: var(--accent); + transition: border-color 0.15s, background 0.15s; + user-select: none; +} +.upstream-new-trigger:hover { + border-color: var(--accent); + background: rgba(13, 148, 136, 0.04); +} +.upstream-new-trigger.active { + border-style: solid; + border-color: var(--accent); + background: rgba(13, 148, 136, 0.06); +} + +.upstream-new-trigger-radio { + width: 16px; + height: 16px; + border-radius: 50%; + border: 1.5px solid var(--border); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: border-color 0.15s; +} +.upstream-new-trigger.active .upstream-new-trigger-radio { + border-color: var(--accent); +} +.upstream-new-trigger-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--accent); + opacity: 0; + transform: scale(0.5); + transition: opacity 0.15s, transform 0.15s; +} +.upstream-new-trigger.active .upstream-new-trigger-dot { + opacity: 1; + transform: scale(1); +} + +/* ── Auth badges ── */ +.upstream-auth-badge { + font-size: 10.5px; + font-weight: 600; + letter-spacing: 0.2px; + padding: 2px 8px; + border-radius: 4px; + white-space: nowrap; + flex-shrink: 0; +} +.upstream-auth-badge.auth-bearer { + background: rgba(88, 166, 255, 0.1); + color: #58a6ff; + border: 1px solid rgba(88, 166, 255, 0.2); +} +.upstream-auth-badge.auth-apikey { + background: rgba(188, 140, 255, 0.1); + color: #bc8cff; + border: 1px solid rgba(188, 140, 255, 0.2); +} +.upstream-auth-badge.auth-none { + background: rgba(139, 148, 158, 0.08); + color: var(--text-muted); + border: 1px solid var(--border-subtle); +} + +/* ── Selected upstream preview (under combobox) ── */ +.upstream-preview { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + background: var(--accent-glow); + border: 1px solid rgba(13, 148, 136, 0.3); + border-radius: 7px; +} + +.upstream-preview-name { + font-size: 13px; + font-weight: 600; + color: #fff; + font-family: 'JetBrains Mono', 'Fira Code', monospace; + letter-spacing: -0.2px; +} + +.upstream-preview-url { + font-size: 11.5px; + color: var(--accent-bright, var(--accent)); + font-family: 'JetBrains Mono', 'Fira Code', monospace; + letter-spacing: -0.2px; + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.upstream-preview-change { + font-size: 11.5px; + font-weight: 500; + color: var(--text-muted); + background: none; + border: 1px solid var(--border); + border-radius: 5px; + padding: 3px 10px; + cursor: pointer; + transition: border-color 0.15s, color 0.15s; + flex-shrink: 0; +} +.upstream-preview-change:hover { border-color: var(--text-secondary); color: var(--text-primary); } + +/* ── Register new upstream inline form ── */ +.upstream-new-form { + background: var(--bg-canvas); + border: 1px solid var(--border); + border-radius: 8px; + overflow: hidden; +} + +.upstream-new-form-inner { + padding: 16px; + display: flex; + flex-direction: column; + gap: 16px; +} + +/* ── Small action buttons ── */ +.btn-primary-sm { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 7px 16px; + font-family: 'Inter', sans-serif; + font-size: 12.5px; + font-weight: 600; + border-radius: 6px; + border: none; + cursor: pointer; + background: var(--accent); + color: #fff; + transition: opacity 0.15s; +} +.btn-primary-sm:hover { opacity: 0.88; } + +.btn-ghost-sm { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 7px 16px; + font-family: 'Inter', sans-serif; + font-size: 12.5px; + font-weight: 500; + border-radius: 6px; + border: 1px solid var(--border); + cursor: pointer; + background: transparent; + color: var(--text-secondary); + transition: border-color 0.15s, color 0.15s; +} +.btn-ghost-sm:hover { border-color: var(--text-secondary); color: var(--text-primary); } + +/* ══════════════════════════════════════════════ + HTTP method picker (Step 4 REST) + ══════════════════════════════════════════════ */ + +.method-grid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 8px; +} + +.method-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + padding: 14px 8px; + background: var(--bg-canvas); + border: 1.5px solid var(--border); + border-radius: 8px; + cursor: pointer; + transition: border-color 0.15s, background 0.15s, box-shadow 0.15s; + font-family: 'Inter', sans-serif; +} + +.method-card:hover { + border-color: var(--accent); + background: rgba(13, 148, 136, 0.04); +} + +.method-card.active { + border-color: var(--accent); + background: var(--accent-glow); + box-shadow: 0 0 0 3px rgba(13, 148, 136, 0.15); +} + +.method-name { + font-size: 14px; + font-weight: 700; + font-family: 'JetBrains Mono', 'Fira Code', monospace; + color: var(--text-primary); + letter-spacing: 0.5px; +} + +.method-card.active .method-name { color: #fff; } + +.method-desc { + font-size: 11px; + color: var(--text-muted); + font-weight: 400; +} + +.method-card.active .method-desc { color: var(--accent-bright, var(--accent)); } + +.method-callout { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 11px 14px; + background: rgba(13, 148, 136, 0.07); + border: 1px solid rgba(13, 148, 136, 0.25); + border-radius: 7px; + font-size: 12.5px; + color: var(--text-secondary); + line-height: 1.55; + margin-top: 4px; +} + +/* ══════════════════════════════════════════════ + GraphQL operation type cards (Step 4 GraphQL) + ══════════════════════════════════════════════ */ + +.gql-type-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.gql-type-card { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 6px; + padding: 16px 18px; + background: var(--bg-canvas); + border: 1.5px solid var(--border); + border-radius: 8px; + cursor: pointer; + transition: border-color 0.15s, background 0.15s, box-shadow 0.15s; + text-align: left; + font-family: 'Inter', sans-serif; +} + +.gql-type-card:hover { + border-color: var(--accent); + background: rgba(13, 148, 136, 0.04); +} + +.gql-type-card.active { + border-color: var(--accent); + background: var(--accent-glow); + box-shadow: 0 0 0 3px rgba(13, 148, 136, 0.15); +} + +.gql-type-keyword { + font-size: 15px; + font-weight: 700; + font-family: 'JetBrains Mono', 'Fira Code', monospace; + color: var(--accent); + letter-spacing: -0.3px; +} + +.gql-type-card.active .gql-type-keyword { color: #5eead4; } + +.gql-type-desc { + font-size: 12px; + color: var(--text-muted); + line-height: 1.4; +} + +.gql-type-card.active .gql-type-desc { color: var(--text-secondary); } + + +/* ══════════════════════════════════════════════ + Protocol card — disabled tag (Streaming) +══════════════════════════════════════════════ */ +.protocol-tag-disabled { + text-decoration: line-through; + color: var(--error, #f87171); + opacity: 0.75; +} + + +/* ══════════════════════════════════════════════ + gRPC — Proto upload +══════════════════════════════════════════════ */ +.proto-dropzone { + border: 1.5px dashed var(--border); + border-radius: 10px; + padding: 36px 24px; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + cursor: pointer; + transition: border-color 0.15s, background 0.15s; + background: var(--bg-canvas); +} +.proto-dropzone:hover, +.proto-dropzone.drag-over { + border-color: var(--accent); + background: rgba(56,139,253,0.05); +} +.proto-dropzone.drag-over { border-style: solid; } + +.proto-dropzone-title { + font-size: 13px; + font-weight: 500; + color: var(--text-primary); +} +.proto-dropzone-sub { + font-size: 12px; + color: var(--text-muted); +} + +.proto-file-info { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + background: rgba(56,139,253,0.07); + border: 1px solid rgba(56,139,253,0.25); + border-radius: 8px; +} +.proto-file-name { + font-family: var(--font-mono, monospace); + font-size: 13px; + color: var(--accent); + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.proto-file-size { + font-size: 11px; + color: var(--text-muted); + white-space: nowrap; +} +.proto-file-change { + background: none; + border: 1px solid var(--border); + border-radius: 5px; + color: var(--text-secondary); + font-size: 11px; + padding: 3px 9px; + cursor: pointer; + white-space: nowrap; + transition: border-color 0.15s, color 0.15s; +} +.proto-file-change:hover { border-color: var(--accent); color: var(--accent); } + + +/* ══════════════════════════════════════════════ + gRPC — Service/method list +══════════════════════════════════════════════ */ +.proto-service-group { + padding-top: 16px; +} +.proto-service-group + .proto-service-group { + border-top: 1px solid var(--border-subtle); + margin-top: 4px; +} + +.proto-service-label { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--text-muted); + margin-bottom: 10px; +} +.proto-service-label svg { opacity: 0.5; } + +.proto-method-grid { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.proto-method-card { + display: flex; + flex-direction: column; + gap: 4px; + padding: 10px 14px; + background: var(--bg-canvas); + border: 1.5px solid var(--border); + border-radius: 8px; + cursor: pointer; + text-align: left; + transition: border-color 0.15s, background 0.15s; + min-width: 160px; +} +.proto-method-card:hover { + border-color: var(--border-muted, var(--accent)); + background: rgba(56,139,253,0.04); +} +.proto-method-card.active { + border-color: var(--accent); + background: rgba(56,139,253,0.08); +} + +.proto-method-name { + font-family: var(--font-mono, monospace); + font-size: 13px; + font-weight: 600; + color: var(--text-primary); +} +.proto-method-card.active .proto-method-name { color: var(--accent); } + +.proto-method-types { + font-size: 11px; + color: var(--text-muted); + display: flex; + align-items: center; + gap: 4px; +} +.proto-method-types svg { opacity: 0.5; flex-shrink: 0; } + +.proto-empty { + padding: 24px; + text-align: center; + color: var(--text-muted); + font-size: 13px; +} + + +/* ══════════════════════════════════════════════ + gRPC — Method signature detail +══════════════════════════════════════════════ */ +.proto-path-row { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 12px; + background: var(--bg-canvas); + border: 1px solid var(--border-subtle); + border-radius: 6px; +} +.proto-path-label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); + white-space: nowrap; +} +.proto-path-value { + font-family: var(--font-mono, monospace); + font-size: 12px; + color: var(--accent); + word-break: break-all; +} + +.proto-sig-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} +@media (max-width: 680px) { + .proto-sig-grid { grid-template-columns: 1fr; } +} + +.proto-sig-col { + display: flex; + flex-direction: column; + gap: 0; + border: 1px solid var(--border-subtle); + border-radius: 8px; + overflow: hidden; +} + +.proto-sig-col-header { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + background: var(--bg-surface); + border-bottom: 1px solid var(--border-subtle); + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-secondary); +} +.proto-sig-col-header svg { opacity: 0.6; } + +.proto-type-badge { + font-family: var(--font-mono, monospace); + font-size: 11px; + background: rgba(56,139,253,0.12); + color: var(--accent); + padding: 1px 6px; + border-radius: 4px; + margin-left: auto; +} + +.proto-field-list { + padding: 6px 0; +} + +.proto-field-row { + display: flex; + align-items: baseline; + justify-content: space-between; + padding: 5px 12px; + font-size: 12px; + border-bottom: 1px solid var(--border-subtle); +} +.proto-field-row:last-child { border-bottom: none; } + +.proto-field-name { + font-family: var(--font-mono, monospace); + color: var(--text-primary); + font-size: 12px; +} +.proto-field-type { + font-family: var(--font-mono, monospace); + font-size: 11px; + color: var(--text-muted); +} + +.proto-field-empty { + padding: 16px 12px; + font-size: 12px; + color: var(--text-secondary); + text-align: center; + line-height: 1.5; +} + + +/* ══════════════════════════════════════════════ + gRPC — Source tabs +══════════════════════════════════════════════ */ +.grpc-source-tabs { + display: flex; + gap: 0; + margin-bottom: 20px; + background: var(--bg-canvas); + border: 1px solid var(--border); + border-radius: 9px; + padding: 3px; +} + +.grpc-source-btn { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 7px; + padding: 8px 14px; + background: none; + border: none; + border-radius: 7px; + font-size: 13px; + font-weight: 500; + color: var(--text-muted); + cursor: pointer; + transition: background 0.15s, color 0.15s; + white-space: nowrap; +} +.grpc-source-btn svg { flex-shrink: 0; opacity: 0.7; } +.grpc-source-btn:hover { color: var(--text-secondary); background: rgba(255,255,255,0.04); } +.grpc-source-btn.active { + background: var(--bg-surface); + color: var(--text-primary); + box-shadow: 0 1px 3px rgba(0,0,0,0.3); +} +.grpc-source-btn.active svg { opacity: 1; } + + +/* ══════════════════════════════════════════════ + gRPC — Reflection panel +══════════════════════════════════════════════ */ +.grpc-reflect-upstream { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + background: var(--bg-canvas); + border: 1px solid var(--border-subtle); + border-radius: 8px; +} +.grpc-reflect-upstream-info { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} +.grpc-reflect-upstream-name { + font-size: 13px; + font-weight: 500; + color: var(--text-primary); +} +.grpc-reflect-upstream-url { + font-family: var(--font-mono, monospace); + font-size: 11px; + color: var(--text-muted); +} + +.grpc-reflect-status { + display: flex; + align-items: center; + gap: 10px; + font-size: 13px; + color: var(--text-secondary); +} +.grpc-reflect-spinner { + width: 14px; + height: 14px; + border: 2px solid var(--border); + border-top-color: var(--accent); + border-radius: 50%; + animation: grpc-spin 0.7s linear infinite; + flex-shrink: 0; +} +@keyframes grpc-spin { to { transform: rotate(360deg); } } + +.grpc-reflect-error-row { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--error, #f87171); +} diff --git a/apps/ui/css/workspace-setup.css b/apps/ui/css/workspace-setup.css new file mode 100644 index 0000000..3638106 --- /dev/null +++ b/apps/ui/css/workspace-setup.css @@ -0,0 +1,223 @@ +.ws-setup-page { + min-height: 100vh; + background: var(--bg-canvas); + display: flex; + flex-direction: column; +} +.ws-setup-topbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 32px; + border-bottom: 1px solid var(--border-subtle); +} +.ws-setup-logo { + display: flex; + align-items: center; + gap: 10px; + text-decoration: none; + color: var(--text-primary); + font-size: 15px; + font-weight: 600; +} +.ws-setup-logo-mark { + width: 30px; + height: 30px; + background: #0d9488; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; +} +.ws-setup-back { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + color: var(--text-muted); + text-decoration: none; + padding: 6px 10px; + border-radius: 6px; + transition: color 0.15s, background 0.15s; +} +.ws-setup-back:hover { color: var(--text-secondary); background: rgba(255,255,255,0.04); } + +.ws-setup-body { + flex: 1; + display: flex; + justify-content: center; + padding: 48px 24px 80px; +} +.ws-setup-container { + width: 100%; + max-width: 600px; +} +.ws-setup-header { margin-bottom: 32px; } +.ws-setup-title { + font-size: 24px; + font-weight: 700; + color: var(--text-primary); + margin-bottom: 6px; +} +.ws-setup-subtitle { + font-size: 14px; + color: var(--text-muted); + line-height: 1.5; +} + +.ws-form-section { + background: var(--bg-surface); + border: 1px solid var(--border); + border-radius: 12px; + padding: 24px; + margin-bottom: 16px; +} +.ws-form-section-title { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 18px; +} + +/* Avatar */ +.ws-avatar-row { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 20px; +} +.ws-avatar-preview { + width: 52px; + height: 52px; + border-radius: 12px; + background: #0d9488; + color: #fff; + font-size: 22px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: background 0.2s; +} +.ws-avatar-hint { font-size: 12px; color: var(--text-muted); line-height: 1.5; } +.ws-color-swatches { display: flex; gap: 6px; margin-top: 6px; } +.ws-color-swatch { + width: 20px; + height: 20px; + border-radius: 5px; + cursor: pointer; + border: 2px solid transparent; + transition: transform 0.1s, border-color 0.1s; +} +.ws-color-swatch:hover { transform: scale(1.15); } +.ws-color-swatch.active { border-color: var(--text-primary); } + +/* Invite rows */ +.invite-rows { display: flex; flex-direction: column; gap: 8px; } +.invite-row { display: flex; align-items: center; gap: 8px; } +.invite-row .form-input { margin-bottom: 0; } +.invite-row-remove { + background: none; + border: none; + padding: 6px; + cursor: pointer; + color: var(--text-muted); + border-radius: 5px; + display: flex; + align-items: center; + transition: color 0.1s, background 0.1s; + flex-shrink: 0; +} +.invite-row-remove:hover { color: var(--error, #f87171); background: rgba(248,113,113,0.08); } + +/* Members */ +.member-row { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 0; + border-bottom: 1px solid var(--border-subtle); +} +.member-row:last-child { border-bottom: none; } +.member-avatar { + width: 30px; + height: 30px; + border-radius: 50%; + background: var(--bg-muted); + border: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 700; + color: var(--text-secondary); + flex-shrink: 0; +} +.member-info { flex: 1; } +.member-name { font-size: 13.5px; color: var(--text-primary); font-weight: 500; } +.member-email { font-size: 12px; color: var(--text-muted); } +.member-last-active { font-size: 12px; color: var(--text-muted); white-space: nowrap; } + +/* Danger zone */ +.danger-zone-action { + display: flex; + align-items: flex-start; + gap: 16px; + padding: 16px 24px; + border-bottom: 1px solid var(--border-subtle); +} +.danger-zone-action:last-child { border-bottom: none; } +.danger-zone-text { flex: 1; } +.danger-zone-title { font-size: 13.5px; font-weight: 500; color: var(--text-primary); margin-bottom: 3px; } +.danger-zone-desc { font-size: 12.5px; color: var(--text-muted); line-height: 1.5; } + +/* Lang switcher */ +.lang-switcher { display: flex; gap: 6px; margin-top: 6px; } +.lang-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 14px; + border-radius: 6px; + border: 1px solid var(--border); + background: var(--bg-canvas); + color: var(--text-secondary); + font-size: 13px; + cursor: pointer; + transition: background 0.15s, border-color 0.15s, color 0.15s; +} +.lang-btn.active { + background: rgba(13,148,136,0.1); + border-color: rgba(13,148,136,0.35); + color: var(--accent); +} +.lang-btn:hover:not(.active) { background: rgba(255,255,255,0.04); color: var(--text-primary); } +.lang-flag { font-size: 14px; } + +/* Actions row */ +.ws-setup-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + margin-top: 24px; +} +.ws-setup-footer-note { + font-size: 12px; + color: var(--text-muted); + text-align: center; + margin-top: 20px; + line-height: 1.5; +} + +/* Roles reference */ +.role-def-row { + display: flex; + align-items: flex-start; + gap: 16px; + padding: 11px 0; + border-bottom: 1px solid var(--border-subtle); +} +.role-def-row:last-child { border-bottom: none; } +.role-def-text { font-size: 13px; color: var(--text-secondary); line-height: 1.5; } diff --git a/apps/ui/data/agents.json b/apps/ui/data/agents.json new file mode 100644 index 0000000..e78cea5 --- /dev/null +++ b/apps/ui/data/agents.json @@ -0,0 +1,58 @@ +[ + { + "id": "ag-001", + "slug": "customer-support", + "display_name": "Customer Support", + "description": "Handles ticket creation, status lookups, and customer data retrieval. Powers the support team's Claude integration.", + "status": "active", + "operation_count": 5, + "operation_ids": ["op_01", "op_02", "op_07", "op_09", "op_10"], + "key_count": 2, + "created_at": "2024-11-15T10:22:00Z", + "created_by": "AT", + "last_called_at": "2025-03-28T18:44:00Z", + "calls_today": 312 + }, + { + "id": "ag-002", + "slug": "sales-pipeline", + "display_name": "Sales Pipeline", + "description": "CRM operations for the sales team: lead creation, deal updates, contact lookup, and company enrichment.", + "status": "active", + "operation_count": 5, + "operation_ids": ["op_01", "op_06", "op_08", "op_11", "op_12"], + "key_count": 1, + "created_at": "2024-11-28T14:10:00Z", + "created_by": "AT", + "last_called_at": "2025-03-29T09:01:00Z", + "calls_today": 541 + }, + { + "id": "ag-003", + "slug": "internal-devops", + "display_name": "Internal DevOps", + "description": "Infrastructure tooling: deployment triggers, service health checks, and incident management workflows.", + "status": "draft", + "operation_count": 3, + "operation_ids": ["op_04", "op_05", "op_11"], + "key_count": 0, + "created_at": "2025-01-08T09:45:00Z", + "created_by": "AT", + "last_called_at": null, + "calls_today": 0 + }, + { + "id": "ag-004", + "slug": "product-analytics", + "display_name": "Product Analytics", + "description": "Queries usage metrics, feature adoption data, and user behaviour reports from the analytics stack.", + "status": "active", + "operation_count": 4, + "operation_ids": ["op_02", "op_03", "op_08", "op_12"], + "key_count": 3, + "created_at": "2025-02-01T11:30:00Z", + "created_by": "AT", + "last_called_at": "2025-03-29T11:22:00Z", + "calls_today": 87 + } +] diff --git a/apps/ui/data/keys.json b/apps/ui/data/keys.json new file mode 100644 index 0000000..146eb79 --- /dev/null +++ b/apps/ui/data/keys.json @@ -0,0 +1,15 @@ +{ + "acme-workspace": [ + { "id": "key_01", "name": "Production", "prefix": "mcp_prod_4f8a…", "scopes": ["read","write","deploy"], "created": "2025-11-15", "lastUsed": "Today", "status": "active" }, + { "id": "key_02", "name": "CI / CD pipeline", "prefix": "mcp_ci_b21c…", "scopes": ["read","deploy"], "created": "2025-12-01", "lastUsed": "2 days ago", "status": "active" }, + { "id": "key_03", "name": "Dev local", "prefix": "mcp_dev_7e3f…", "scopes": ["read","write"], "created": "2026-01-08", "lastUsed": "Never", "status": "active" }, + { "id": "key_04", "name": "Legacy integration", "prefix": "mcp_leg_c90d…", "scopes": ["read"], "created": "2025-09-22", "lastUsed": "45 days ago", "status": "revoked" } + ], + "startup-demo": [ + { "id": "key_11", "name": "Backend service", "prefix": "mcp_back_9a1b…", "scopes": ["read","write"], "created": "2026-01-20", "lastUsed": "Today", "status": "active" }, + { "id": "key_12", "name": "Analytics pipeline", "prefix": "mcp_anal_3c7d…", "scopes": ["read"], "created": "2026-02-05", "lastUsed": "3 days ago", "status": "active" } + ], + "personal": [ + { "id": "key_21", "name": "Local experiments", "prefix": "mcp_loc_f2e8…", "scopes": ["read","write","deploy"], "created": "2026-03-01", "lastUsed": "Yesterday", "status": "active" } + ] +} diff --git a/apps/ui/data/operations.json b/apps/ui/data/operations.json new file mode 100644 index 0000000..c0d4cd9 --- /dev/null +++ b/apps/ui/data/operations.json @@ -0,0 +1,134 @@ +[ + { + "id": "op_01", + "name": "create_crm_lead", + "display_name": "Create CRM Lead", + "protocol": "rest", + "method": "POST", + "status": "active", + "category": "CRM", + "target_url": "https://api.acme.com/v2/crm/leads", + "created_at": "2026-03-21" + }, + { + "id": "op_02", + "name": "get_user_profile", + "display_name": "Get User Profile", + "protocol": "rest", + "method": "GET", + "status": "active", + "category": "Users", + "target_url": "https://api.acme.com/v1/users/{id}", + "created_at": "2026-03-18" + }, + { + "id": "op_03", + "name": "search_knowledge_base", + "display_name": "Search Knowledge Base", + "protocol": "graphql", + "method": null, + "status": "active", + "category": "Search", + "target_url": "https://graph.notion-int.acme.com/graphql", + "created_at": "2026-03-14" + }, + { + "id": "op_04", + "name": "send_slack_message", + "display_name": "Send Slack Message", + "protocol": "rest", + "method": "POST", + "status": "error", + "category": "Notifications", + "target_url": "https://slack.com/api/chat.postMessage", + "created_at": "2026-03-10" + }, + { + "id": "op_05", + "name": "stream_telemetry", + "display_name": "Stream Telemetry Events", + "protocol": "grpc", + "method": null, + "status": "draft", + "category": "Observability", + "target_url": "grpc://telemetry.internal.acme.com:9090", + "created_at": "2026-03-05" + }, + { + "id": "op_06", + "name": "update_deal_stage", + "display_name": "Update Deal Stage", + "protocol": "rest", + "method": "PATCH", + "status": "inactive", + "category": "CRM", + "target_url": "https://api.acme.com/v2/crm/deals/{id}", + "created_at": "2026-02-28" + }, + { + "id": "op_07", + "name": "fetch_invoice", + "display_name": "Fetch Invoice", + "protocol": "rest", + "method": "GET", + "status": "active", + "category": "Billing", + "target_url": "https://billing.acme.com/v1/invoices/{id}", + "created_at": "2026-02-20" + }, + { + "id": "op_08", + "name": "list_products", + "display_name": "List Products", + "protocol": "graphql", + "method": null, + "status": "active", + "category": "Catalog", + "target_url": "https://shop.acme.com/graphql", + "created_at": "2026-02-15" + }, + { + "id": "op_09", + "name": "create_support_ticket", + "display_name": "Create Support Ticket", + "protocol": "rest", + "method": "POST", + "status": "active", + "category": "Support", + "target_url": "https://support.acme.com/api/tickets", + "created_at": "2026-02-10" + }, + { + "id": "op_10", + "name": "get_order_status", + "display_name": "Get Order Status", + "protocol": "rest", + "method": "GET", + "status": "draft", + "category": "Orders", + "target_url": "https://orders.acme.com/v2/status/{id}", + "created_at": "2026-02-05" + }, + { + "id": "op_11", + "name": "sync_contacts", + "display_name": "Sync Contacts", + "protocol": "grpc", + "method": null, + "status": "active", + "category": "CRM", + "target_url": "grpc://contacts.internal.acme.com:9091", + "created_at": "2026-01-28" + }, + { + "id": "op_12", + "name": "send_email_campaign", + "display_name": "Send Email Campaign", + "protocol": "rest", + "method": "POST", + "status": "active", + "category": "Marketing", + "target_url": "https://mail.acme.com/api/v3/campaigns/send", + "created_at": "2026-01-20" + } +] diff --git a/apps/ui/html/agents.html b/apps/ui/html/agents.html new file mode 100644 index 0000000..61bc34f --- /dev/null +++ b/apps/ui/html/agents.html @@ -0,0 +1,380 @@ + + + + + + + Crank — Agents + + + + + + + + + + + + + + + + +
+ Operations + Agents + API Keys + Logs + Usage +
+ + +
+ + + + + +
+
+
Total agents
+
+
across this workspace
+
+
+
Active
+
+
+
+
+
Operations exposed
+
+
+
+
+
Calls today
+
+
all agents combined
+
+
+ + +
+ +
+ + of agents + +
+
+ + +
+ + 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 ? operations in this workspace. +
+ + +
+
+ Loading agents… +
+ + +
+
+ +
+
No agents yet
+
Create your first agent to get a dedicated MCP endpoint with a curated set of tools.
+ +
+ + +
+
No agents match ""
+
Try a different search term.
+
+ + +
+ +
+ +
+ + + + + + + + + + + + + + diff --git a/apps/ui/html/api-keys.html b/apps/ui/html/api-keys.html new file mode 100644 index 0000000..daeff65 --- /dev/null +++ b/apps/ui/html/api-keys.html @@ -0,0 +1,281 @@ + + + + + + + Crank — API Keys + + + + + + + + + + + + + + + +
+ Operations + Agents + API Keys + Logs + Usage +
+ + +
+ + + +
+ + + + +
+ Keys are only shown once. Copy and store them securely immediately after creation — Crank stores only a hash. Revoked keys cannot be restored. +
+
+ +
+
+
+
Active keys
+
3 active · 1 revoked
+
+ +
+ +
+ + + + + + + + + + + + + + +
NameKey prefixScopesCreatedLast usedStatus
+
+
+ +
+
+
Scope reference
+
+
+
+
+ read +
+
List operations, read configs, query logs and usage metrics.
+
+
+
+ write +
+
Create, update, and delete operations and their configurations.
+
+
+
+ deploy +
+
Publish draft operations, rollback deployments, and manage traffic splits.
+
+
+
+ +
+ + + + + + + + + + + + + + + diff --git a/apps/ui/html/fragments/lang-switcher.html b/apps/ui/html/fragments/lang-switcher.html new file mode 100644 index 0000000..96a8403 --- /dev/null +++ b/apps/ui/html/fragments/lang-switcher.html @@ -0,0 +1,13 @@ +
+ +
+ + +
+
diff --git a/apps/ui/html/login.html b/apps/ui/html/login.html new file mode 100644 index 0000000..e987eab --- /dev/null +++ b/apps/ui/html/login.html @@ -0,0 +1,78 @@ + + + + + + + Crank — Sign in + + + + + + + + +
+ +
+ + + + + diff --git a/apps/ui/html/logs.html b/apps/ui/html/logs.html new file mode 100644 index 0000000..efbdc0c --- /dev/null +++ b/apps/ui/html/logs.html @@ -0,0 +1,148 @@ + + + + + + + Crank — Logs + + + + + + + + + + + + + + +
+ Operations + Agents + API Keys + Logs + Usage +
+ +
+ + + +
+
+
+ Live +
+ + + +
+ + + + + + + + + +
+ +
+
+
+ +
+ + + + + + diff --git a/apps/ui/html/settings.html b/apps/ui/html/settings.html new file mode 100644 index 0000000..b6a8d1c --- /dev/null +++ b/apps/ui/html/settings.html @@ -0,0 +1,597 @@ + + + + + + + Crank — Settings + + + + + + + + + + + + + + +
+ Operations + Agents + API Keys + Logs + Usage +
+ +
+ + + +
+ + + + + +
+ + + + +
+
+
+
Profile
+
+
+
+
AT
+
+
Operator
+
operator@acme-workspace
+ +
+
+
Avatar will appear in the console and team views.
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
Changing email requires re-verification. You'll receive a confirmation link.
+
+ +
+ +
+
+
+
+ + + + + + + +
+
+
+
Security
+
+
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ +
+
+ +
+
+
Two-factor authentication
+ +
+
+
TOTP authenticator app
+
Use Google Authenticator, 1Password, or any TOTP-compatible app.
+
+ Enabled + +
+ +
+
+
Hardware security key
+
FIDO2 / WebAuthn passkey or YubiKey.
+
+ +
+
+
+ +
+
+
Active sessions
+
+
+
+
+
Chrome · macOS — current
+
Last active: now · 93.184.216.34 (US)
+
+
+
+
+
Safari · iPhone
+
Last active: 2 days ago · 93.184.216.34 (US)
+
+ +
+
+
+
Firefox · Linux
+
Last active: 8 days ago · 172.16.0.4 (Internal)
+
+ +
+
+
+
+ + +
+
+
+
Notification preferences
+
+
+
+
+
Operation error spike
+
Alert when error rate exceeds 5% over a 5-minute window.
+
+
+
+
+
+
Upstream latency degradation
+
Alert when p99 latency exceeds the baseline by 3×.
+
+
+
+
+
+
Quota approaching limit
+
Email when any operation reaches 80% of its monthly quota.
+
+
+
+
+
+
New team member joined
+
Notify workspace admins when someone accepts an invite.
+
+
+
+
+
+
Weekly usage digest
+
Email summary of invocations, errors and latency each Monday.
+
+
+
+
+
+
+ + + + +
+
+ +
+ + + + + + diff --git a/apps/ui/html/usage.html b/apps/ui/html/usage.html new file mode 100644 index 0000000..88380bb --- /dev/null +++ b/apps/ui/html/usage.html @@ -0,0 +1,228 @@ + + + + + + + Crank — Usage + + + + + + + + + + + + + + +
+ Operations + Agents + API Keys + Logs + Usage +
+ +
+ + + + +
+
+
Total invocations
+
28,341
+
+ + +12% vs prior period +
+
+
+
Success rate
+
98.6%
+
+ + +0.3pp vs prior period +
+
+
+
Median latency (p50)
+
124ms
+
+ + -18ms vs prior period +
+
+
+
p99 latency
+
2.1s
+
+ + +340ms vs prior period +
+
+
+ + +
+
+
Invocations over time
+
+ Success + Error +
+
+
+
+
+
+ + +
+
+
+
By operation
+
Breakdown for last 7 days
+
+
+ + + + + + + + + + + + + + +
OperationProtocolCallsErrorsError rateLatency (p50 / p95 / p99)Quota
+
+ +
+ + + + + + + + + + + + diff --git a/apps/ui/html/wizard/index.html b/apps/ui/html/wizard/index.html new file mode 100644 index 0000000..8862771 --- /dev/null +++ b/apps/ui/html/wizard/index.html @@ -0,0 +1,238 @@ + + + + + + Crank — New Operation + + + + + + + + + + + + + + + +
+ Operations + Agents + API Keys + Logs + Usage +
+ + +
+ + +
+ +
+ Create operation +
+
+
+ 20% + +
+ + +
+ + +
+
+ +
+
M
+
+ Create operation +
+
+ +
+ + +
+ +
+
1
+
+
Step 1
+
Protocol
+
In progress
+
+
+ +
+
2
+
+
Step 2
+
Upstream target
+
Not started
+
+
+ + + +
+
4
+
+
Step 4
+
Tool config
+
Not started
+
+
+ +
+
5
+
+
Step 5
+
Mapping
+
Not started
+
+
+ +
+ + +
+
+
+ + +
+
+ + + + +
+ + + + + + diff --git a/apps/ui/html/wizard/step1.html b/apps/ui/html/wizard/step1.html new file mode 100644 index 0000000..3a843e5 --- /dev/null +++ b/apps/ui/html/wizard/step1.html @@ -0,0 +1,95 @@ + +
+
+
+
+ Step 1 of 5 +
+

Choose a protocol

+

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.

+
+ + +
+ + + + + + + +
+ + +
+ + + + +
+
You can change this later
+
+ Switching protocol after configuring subsequent steps will clear schema and mapping data. Save your operation as a draft before experimenting. See protocol migration guide for details. +
+
+
+ +
diff --git a/apps/ui/html/wizard/step2.html b/apps/ui/html/wizard/step2.html new file mode 100644 index 0000000..a5e02a0 --- /dev/null +++ b/apps/ui/html/wizard/step2.html @@ -0,0 +1,157 @@ + +
+
+
+
+ Step 2 of 5 +
+

Select upstream & endpoint

+

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.

+
+ + +
+
+
+ + + + + + +
+
+
Upstream
+
Shared host, base URL, and authentication
+
+ Required +
+
+ + +
+
+
+ Select an upstream… +
+ + + +
+ + + +
+ + + + + +
+
+
+
+ + + + Register new upstream +
+ + + + +
+
+ + +
+
+
+ + + +
+
+
Endpoint
+
Path and HTTP method for this specific operation
+
+ Required +
+
+
+ + +
Appended to the upstream base URL. Use {param} for path variables.
+
+
+
+ +
+ + + + + + diff --git a/apps/ui/html/wizard/step3-graphql.html b/apps/ui/html/wizard/step3-graphql.html new file mode 100644 index 0000000..c2335dd --- /dev/null +++ b/apps/ui/html/wizard/step3-graphql.html @@ -0,0 +1,92 @@ +
+
+
+
+ Step 3 of 5 +
+

GraphQL operation

+

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.

+
+ + +
+
+
+ + + + +
+
+
Operation type
+
GraphQL operation kind — subscriptions are not supported
+
+ Required +
+
+
+ + +
+
+
+ + +
+
+
+ + + + +
+
+
Query document
+
GraphQL document with typed variable declarations
+
+ Required +
+
+
+
+
+ graphql / query +
+ +
+
+
+ + +
+ + + + +
+
Variables are mapped in step 5
+
+ Declare all variables in the query document using the $variable: Type syntax. In step 5 you will map MCP input fields to these variables. Crank serialises the final {"query","variables"} payload automatically. +
+
+
+ +
diff --git a/apps/ui/html/wizard/step3-grpc.html b/apps/ui/html/wizard/step3-grpc.html new file mode 100644 index 0000000..0146128 --- /dev/null +++ b/apps/ui/html/wizard/step3-grpc.html @@ -0,0 +1,274 @@ +
+
+
+
+ Step 3 of 5 +
+

RPC method

+

Choose how to describe the gRPC service contract. Upload a .proto 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.

+
+ + +
+ + + +
+ + +
+
+
+
+ +
+
+
Proto definition
+
Upload or paste your .proto file
+
+ Required +
+
+
+ + + + +
Drop .proto file here
+
or click to browse
+ +
+ +
+
+ +
+
+ +
+
+ +
+ + + + + + + + + + +
+ + + + + + + + + diff --git a/apps/ui/html/wizard/step3-rest.html b/apps/ui/html/wizard/step3-rest.html new file mode 100644 index 0000000..de94ea3 --- /dev/null +++ b/apps/ui/html/wizard/step3-rest.html @@ -0,0 +1,93 @@ + +
+
+
+
+ Step 3 of 5 +
+

HTTP method & format

+

Choose the HTTP verb this operation sends and configure content negotiation headers. Crank will serialize MCP tool arguments into the appropriate request body format.

+
+ + +
+
+
+ + + +
+
+
HTTP method
+
Verb sent to the upstream on every tool invocation
+
+ Required +
+
+
+ + + + + +
+ +
+
+ + +
+
+
+ + + + +
+
+
Request format
+
Content negotiation and serialisation
+
+ Optional +
+
+
+
+ + +
How the request body is encoded when sent to the upstream.
+
+
+ + +
Accepted response content types from the upstream server.
+
+
+
+
+ +
diff --git a/apps/ui/html/wizard/step4.html b/apps/ui/html/wizard/step4.html new file mode 100644 index 0000000..a14377f --- /dev/null +++ b/apps/ui/html/wizard/step4.html @@ -0,0 +1,162 @@ +
+
+
+
+ Step 4 of 5 +
+

Tool config

+

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.

+
+ +
+
+
+ + + +
+
+
Tool identity
+
Machine-readable name and LLM-facing description
+
+
+
+ +
+ + +
Machine-readable identifier used in MCP tool calls. Only lowercase letters, numbers and underscores. Cannot be changed after publishing.
+
+ +
+
+ + +
Human-readable name shown in the console and audit log.
+
+
+ + +
Short imperative sentence shown in the MCP tool manifest.
+
+
+ +
+ + +
LLM-facing description. Be precise — this is the primary signal the model uses to decide whether to invoke this tool.
+
+ +
+
+ +
+ + + + +
+
Writing effective descriptions
+
+ Start with a verb ("Creates", "Fetches", "Updates"). Mention key input requirements. Describe what a successful response looks like. Avoid vague phrasing like "handles" or "manages". See description best practices → +
+
+
+ +
+
+
+ + + + +
+
+
Input schema
+
Parameters the LLM passes when calling this tool
+
+ JSON Schema draft-07 +
+
+
+
+
+ json / input-schema +
+ +
+
+
+ +
+
+
+ + + + +
+
+
Output schema
+
Shape of the data returned to the LLM after a successful call
+
+ JSON Schema draft-07 +
+
+
+
+
+ json / output-schema +
+ +
+
+
+ +
+ + + + +
+
Schemas are protocol-agnostic
+
+ 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. +
+
+
+
diff --git a/apps/ui/html/wizard/step5.html b/apps/ui/html/wizard/step5.html new file mode 100644 index 0000000..13c4d25 --- /dev/null +++ b/apps/ui/html/wizard/step5.html @@ -0,0 +1,102 @@ +
+
+
+
+ Step 5 of 5 +
+

Mapping and execution

+

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.

+
+ +
+ Input → Request +
+
+ +
+
+
+
+
+ yaml / input-mapping +
+ +
+
+
+ +
+ Response → Output +
+
+ +
+
+
+
+
+ yaml / output-mapping +
+ +
+
+
+ +
+ Execution +
+
+ +
+
+
+ + + + +
+
+
Execution config
+
Timeouts, retry policy and auth profile reference
+
+ Optional +
+
+
+
+
+ yaml / exec-config +
+ +
+
+
+ +
+ + + + +
+
Operation will be saved as a draft
+
+ 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. +
+
+
+
diff --git a/apps/ui/html/workspace-setup.html b/apps/ui/html/workspace-setup.html new file mode 100644 index 0000000..6aab8a5 --- /dev/null +++ b/apps/ui/html/workspace-setup.html @@ -0,0 +1,327 @@ + + + + + + + Crank — Workspace + + + + + + + + + + + +
+ + + + + +
+
+ +
+
Workspace settings
+
Configure your workspace identity, default settings, and team members.
+
+ + +
+
Workspace identity
+ +
+
A
+
+
Avatar is derived from your workspace name.
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + +
+ +
+ +
+ mcp.crank.io/ + +
+
Only lowercase letters, numbers, and hyphens. Used in MCP endpoint URLs.
+
+ +
+ + +
+
+ + +
+
Default settings
+ +
+
+ + +
Pre-selected in the operation wizard.
+
+
+ + +
+
+ +
+ +
+ + +
+
+
+ + + + + + + + +
+ Cancel + +
+ + + + + + +
+
+ +
+ + + + + diff --git a/apps/ui/icons/agents/agent.svg b/apps/ui/icons/agents/agent.svg new file mode 100644 index 0000000..7abbba5 --- /dev/null +++ b/apps/ui/icons/agents/agent.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/ui/icons/agents/eye.svg b/apps/ui/icons/agents/eye.svg new file mode 100644 index 0000000..f843b1b --- /dev/null +++ b/apps/ui/icons/agents/eye.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/ui/icons/agents/key.svg b/apps/ui/icons/agents/key.svg new file mode 100644 index 0000000..c208db3 --- /dev/null +++ b/apps/ui/icons/agents/key.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/general/arrow-down.svg b/apps/ui/icons/general/arrow-down.svg new file mode 100644 index 0000000..7577633 --- /dev/null +++ b/apps/ui/icons/general/arrow-down.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/general/arrow-left.svg b/apps/ui/icons/general/arrow-left.svg new file mode 100644 index 0000000..a34bc94 --- /dev/null +++ b/apps/ui/icons/general/arrow-left.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/general/arrow-right.svg b/apps/ui/icons/general/arrow-right.svg new file mode 100644 index 0000000..71e439f --- /dev/null +++ b/apps/ui/icons/general/arrow-right.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/general/arrow-up.svg b/apps/ui/icons/general/arrow-up.svg new file mode 100644 index 0000000..85d9676 --- /dev/null +++ b/apps/ui/icons/general/arrow-up.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/general/bell.svg b/apps/ui/icons/general/bell.svg new file mode 100644 index 0000000..7ccd650 --- /dev/null +++ b/apps/ui/icons/general/bell.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/general/check-sm.svg b/apps/ui/icons/general/check-sm.svg new file mode 100644 index 0000000..3449e27 --- /dev/null +++ b/apps/ui/icons/general/check-sm.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/general/check.svg b/apps/ui/icons/general/check.svg new file mode 100644 index 0000000..250df56 --- /dev/null +++ b/apps/ui/icons/general/check.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/general/chevron-down.svg b/apps/ui/icons/general/chevron-down.svg new file mode 100644 index 0000000..07aaf73 --- /dev/null +++ b/apps/ui/icons/general/chevron-down.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/general/close.svg b/apps/ui/icons/general/close.svg new file mode 100644 index 0000000..648c2e9 --- /dev/null +++ b/apps/ui/icons/general/close.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/general/copy.svg b/apps/ui/icons/general/copy.svg new file mode 100644 index 0000000..3a586b9 --- /dev/null +++ b/apps/ui/icons/general/copy.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/general/crank-logo.svg b/apps/ui/icons/general/crank-logo.svg new file mode 100644 index 0000000..b33e89f --- /dev/null +++ b/apps/ui/icons/general/crank-logo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/apps/ui/icons/general/download.svg b/apps/ui/icons/general/download.svg new file mode 100644 index 0000000..494bbb5 --- /dev/null +++ b/apps/ui/icons/general/download.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/general/edit.svg b/apps/ui/icons/general/edit.svg new file mode 100644 index 0000000..0a2419e --- /dev/null +++ b/apps/ui/icons/general/edit.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/general/external.svg b/apps/ui/icons/general/external.svg new file mode 100644 index 0000000..893d2df --- /dev/null +++ b/apps/ui/icons/general/external.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/general/grid.svg b/apps/ui/icons/general/grid.svg new file mode 100644 index 0000000..98effa2 --- /dev/null +++ b/apps/ui/icons/general/grid.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/general/info-circle.svg b/apps/ui/icons/general/info-circle.svg new file mode 100644 index 0000000..cdc8fd5 --- /dev/null +++ b/apps/ui/icons/general/info-circle.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/ui/icons/general/info.svg b/apps/ui/icons/general/info.svg new file mode 100644 index 0000000..1270118 --- /dev/null +++ b/apps/ui/icons/general/info.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/general/logout.svg b/apps/ui/icons/general/logout.svg new file mode 100644 index 0000000..a769622 --- /dev/null +++ b/apps/ui/icons/general/logout.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/ui/icons/general/plus-sm.svg b/apps/ui/icons/general/plus-sm.svg new file mode 100644 index 0000000..bd555c4 --- /dev/null +++ b/apps/ui/icons/general/plus-sm.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/general/plus.svg b/apps/ui/icons/general/plus.svg new file mode 100644 index 0000000..f192806 --- /dev/null +++ b/apps/ui/icons/general/plus.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/general/refresh.svg b/apps/ui/icons/general/refresh.svg new file mode 100644 index 0000000..a64b2de --- /dev/null +++ b/apps/ui/icons/general/refresh.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/general/search.svg b/apps/ui/icons/general/search.svg new file mode 100644 index 0000000..ac71407 --- /dev/null +++ b/apps/ui/icons/general/search.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/general/settings.svg b/apps/ui/icons/general/settings.svg new file mode 100644 index 0000000..556e1cd --- /dev/null +++ b/apps/ui/icons/general/settings.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/ui/icons/general/trash.svg b/apps/ui/icons/general/trash.svg new file mode 100644 index 0000000..658080d --- /dev/null +++ b/apps/ui/icons/general/trash.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/general/user.svg b/apps/ui/icons/general/user.svg new file mode 100644 index 0000000..e9ef4cd --- /dev/null +++ b/apps/ui/icons/general/user.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/general/warning.svg b/apps/ui/icons/general/warning.svg new file mode 100644 index 0000000..3b96ca1 --- /dev/null +++ b/apps/ui/icons/general/warning.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/ui/icons/login/google.svg b/apps/ui/icons/login/google.svg new file mode 100644 index 0000000..19c999b --- /dev/null +++ b/apps/ui/icons/login/google.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/ui/icons/wizard/clock.svg b/apps/ui/icons/wizard/clock.svg new file mode 100644 index 0000000..2b3ba0b --- /dev/null +++ b/apps/ui/icons/wizard/clock.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/ui/icons/wizard/code.svg b/apps/ui/icons/wizard/code.svg new file mode 100644 index 0000000..31327ef --- /dev/null +++ b/apps/ui/icons/wizard/code.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/wizard/file.svg b/apps/ui/icons/wizard/file.svg new file mode 100644 index 0000000..aee4e64 --- /dev/null +++ b/apps/ui/icons/wizard/file.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/wizard/graphql-protocol.svg b/apps/ui/icons/wizard/graphql-protocol.svg new file mode 100644 index 0000000..08a5fa5 --- /dev/null +++ b/apps/ui/icons/wizard/graphql-protocol.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/apps/ui/icons/wizard/grpc-protocol.svg b/apps/ui/icons/wizard/grpc-protocol.svg new file mode 100644 index 0000000..bde9d0a --- /dev/null +++ b/apps/ui/icons/wizard/grpc-protocol.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/apps/ui/icons/wizard/reflection.svg b/apps/ui/icons/wizard/reflection.svg new file mode 100644 index 0000000..19f923f --- /dev/null +++ b/apps/ui/icons/wizard/reflection.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/ui/icons/wizard/rest-protocol.svg b/apps/ui/icons/wizard/rest-protocol.svg new file mode 100644 index 0000000..97bc44e --- /dev/null +++ b/apps/ui/icons/wizard/rest-protocol.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/icons/wizard/server.svg b/apps/ui/icons/wizard/server.svg new file mode 100644 index 0000000..e58ca74 --- /dev/null +++ b/apps/ui/icons/wizard/server.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/ui/icons/wizard/upload.svg b/apps/ui/icons/wizard/upload.svg new file mode 100644 index 0000000..1dc7232 --- /dev/null +++ b/apps/ui/icons/wizard/upload.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/ui/index.html b/apps/ui/index.html index 81d9d27..88c2b92 100644 --- a/apps/ui/index.html +++ b/apps/ui/index.html @@ -1,94 +1,418 @@ - + - - - - Crank Console - - - -
- Crank logo -
UI replacement in progress
-

Crank Console

-

- The legacy React/Vite UI has been removed. A new operator console is being integrated on top of the same - deployment path. -

-

- Backend services, MCP endpoints and deployment contracts remain intact. Replace this static entrypoint with - the new Alpine.js UI when it is ready. -

-
- - + +
+ + +
+
+
Operator
+
Admin · acme-workspace
+
+ + + +
+
+ + + + +
+ Operations + Agents + API Keys + Logs + Usage +
+ + +
+ + + + + +
+
+
Total operations
+
+
+3 this month
+
+
+
Active
+
+
+
+
+
Requests today
+
4,821
+
+12% vs yesterday
+
+
+
Avg. latency
+
187ms
+
+23ms this week
+
+
+ + +
+ +
+ + +
+ + + + + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + Showing of + + + +
+ + +
+
+ +
+ + +
+ + +
+ + +
+ + +
+

Loading operations…

+
+ + +
+

No operations found

+

Try adjusting your search or filters.

+
+ + + + + + + + + + + + + + + + +
+ NAME + + + + PROTOCOLSTATUS + CREATED + + + + TARGET URL
+ + + + +
+ +
+ + + + + + diff --git a/apps/ui/js/agents.js b/apps/ui/js/agents.js new file mode 100644 index 0000000..3bcb663 --- /dev/null +++ b/apps/ui/js/agents.js @@ -0,0 +1,208 @@ +document.addEventListener('alpine:init', () => { + Alpine.data('agents', () => ({ + agents: [], + operations: [], + loading: true, + + agentSearch: '', + openDropdown: null, + + // drawer state + drawerOpen: false, + drawerMode: 'create', // 'create' | 'edit' + editingId: null, + + // form fields + form: { + display_name: '', + slug: '', + description: '', + status: 'active', + selectedOps: [], + }, + + // operations picker + opSearch: '', + slugManuallyEdited: false, + + async init() { + const [agentsRes, opsRes] = await Promise.all([ + fetch((window.DATA_URL||'data/')+'agents.json'), + fetch((window.DATA_URL||'data/')+'operations.json'), + ]); + this.agents = await agentsRes.json(); + this.operations = await opsRes.json(); + this.loading = false; + + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && this.drawerOpen) this.closeDrawer(); + }); + + document.addEventListener('click', () => { this.openDropdown = null; }); + }, + + // ── computed ── + get filteredAgents() { + const q = this.agentSearch.toLowerCase().trim(); + if (!q) return this.agents; + return this.agents.filter(a => + a.display_name.toLowerCase().includes(q) || + a.slug.toLowerCase().includes(q) || + (a.description || '').toLowerCase().includes(q) + ); + }, + + get totalCalls() { + return this.agents.reduce((s, a) => s + (a.calls_today || 0), 0); + }, + get activeCount() { + return this.agents.filter(a => a.status === 'active').length; + }, + get totalOpsExposed() { + return this.agents.reduce((s, a) => s + (a.operation_count || 0), 0); + }, + + get filteredOps() { + const q = this.opSearch.toLowerCase(); + if (!q) return this.operations; + return this.operations.filter(op => + op.name.toLowerCase().includes(q) || + op.display_name.toLowerCase().includes(q) + ); + }, + + // ── drawer ── + openCreate() { + this.drawerMode = 'create'; + this.editingId = null; + this.form = { display_name: '', slug: '', description: '', status: 'active', selectedOps: [] }; + this.opSearch = ''; + this.slugManuallyEdited = false; + this.drawerOpen = true; + }, + + openEdit(agent) { + this.drawerMode = 'edit'; + this.editingId = agent.id; + this.form = { + display_name: agent.display_name, + slug: agent.slug, + description: agent.description, + status: agent.status, + selectedOps: [...(agent.operation_ids || [])], + }; + this.opSearch = ''; + this.slugManuallyEdited = true; + this.drawerOpen = true; + }, + + closeDrawer() { + this.drawerOpen = false; + }, + + // ── slug auto-derive ── + onNameInput(val) { + this.form.display_name = val; + if (!this.slugManuallyEdited) { + this.form.slug = val.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''); + } + }, + + onSlugInput(val) { + this.form.slug = val.toLowerCase().replace(/[^a-z0-9-]/g, ''); + this.slugManuallyEdited = true; + }, + + // ── operations picker ── + toggleOp(opId) { + const idx = this.form.selectedOps.indexOf(opId); + if (idx === -1) this.form.selectedOps.push(opId); + else this.form.selectedOps.splice(idx, 1); + }, + isOpSelected(opId) { + return this.form.selectedOps.includes(opId); + }, + + // ── save ── + saveAgent() { + if (!this.form.display_name.trim() || !this.form.slug.trim()) return; + + const ws = (localStorage.getItem('crank_workspace') || 'acme-workspace'); + const endpoint = '/mcp/v1/' + ws + '/' + this.form.slug; + + if (this.drawerMode === 'create') { + this.agents.unshift({ + id: 'ag-' + Date.now(), + slug: this.form.slug, + display_name: this.form.display_name, + description: this.form.description, + status: this.form.status, + operation_count: this.form.selectedOps.length, + operation_ids: [...this.form.selectedOps], + key_count: 0, + created_at: new Date().toISOString(), + created_by: 'AT', + last_called_at: null, + calls_today: 0, + mcp_endpoint: endpoint, + }); + } else { + const idx = this.agents.findIndex(a => a.id === this.editingId); + if (idx !== -1) { + this.agents[idx] = Object.assign({}, this.agents[idx], { + display_name: this.form.display_name, + slug: this.form.slug, + description: this.form.description, + status: this.form.status, + operation_count: this.form.selectedOps.length, + operation_ids: [...this.form.selectedOps], + mcp_endpoint: endpoint, + }); + } + } + this.closeDrawer(); + }, + + // ── delete ── + deleteAgent(id) { + this.agents = this.agents.filter(a => a.id !== id); + }, + + // ── helpers ── + mcpEndpoint(agent) { + if (agent.mcp_endpoint) return agent.mcp_endpoint; + const ws = localStorage.getItem('crank_workspace') || 'acme-workspace'; + return '/mcp/v1/' + ws + '/' + agent.slug; + }, + + copyEndpoint(agent) { + navigator.clipboard.writeText(this.mcpEndpoint(agent)).catch(() => {}); + }, + + statusClass(status) { + return 'agent-status-badge agent-status-' + status; + }, + + protocolBadge(op) { + if (op.protocol === 'rest') return 'badge badge-rest'; + if (op.protocol === 'graphql') return 'badge badge-graphql'; + return 'badge badge-grpc'; + }, + + protocolLabel(op) { + if (op.protocol === 'rest') return 'REST'; + if (op.protocol === 'graphql') return 'GQL'; + return 'gRPC'; + }, + + formatDate(dateStr) { + if (!dateStr) return '—'; + return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); + }, + + formatCalls(n) { + if (!n) return '0'; + return n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n); + }, + })); +}); diff --git a/apps/ui/js/api-keys.js b/apps/ui/js/api-keys.js new file mode 100644 index 0000000..f21cc5a --- /dev/null +++ b/apps/ui/js/api-keys.js @@ -0,0 +1,241 @@ +/* ═══════════════════════════════════════════════════ + Crank — API Keys page + Data: GET /api/workspaces/{slug}/keys + ═══════════════════════════════════════════════════ */ + +var KEYS = []; +var currentWorkspaceSlug = ''; + +var SCOPES = ['read', 'write', 'deploy']; +var SCOPE_DESC = { + read: 'Read operations, logs, and usage', + write: 'Create and update operations', + deploy: 'Publish and rollback deployments', +}; + +var selectedScopes = new Set(['read']); +var search = ''; + +// ── API helpers ────────────────────────────────────────────────────────────── + +function apiBase() { + return '/api/workspaces/' + currentWorkspaceSlug + '/keys'; +} + +// In production: replace fetch(mockUrl()) with fetch(apiBase()) +function mockUrl() { + return (window.DATA_URL || 'data/') + 'keys.json'; +} + +// ── Load keys for current workspace ───────────────────────────────────────── + +function loadKeys() { + currentWorkspaceSlug = getCurrentWs().slug; + + setTableLoading(true); + + // Production: fetch(apiBase()) + fetch(mockUrl()) + .then(function(r) { + if (!r.ok) throw new Error('HTTP ' + r.status); + return r.json(); + }) + .then(function(data) { + // Mock JSON is keyed by workspace slug; real API returns array directly + KEYS = Array.isArray(data) ? data : (data[currentWorkspaceSlug] || []); + setTableLoading(false); + renderTable(); + }) + .catch(function() { + KEYS = []; + setTableLoading(false); + renderTable(); + }); +} + +function setTableLoading(on) { + var tbody = document.getElementById('keys-tbody'); + if (!on) return; + tbody.innerHTML = ''; + var tr = document.createElement('tr'); + var td = document.createElement('td'); + td.colSpan = 7; + td.style.cssText = 'text-align:center;padding:36px;color:var(--text-muted);'; + td.textContent = 'Loading…'; + tr.appendChild(td); + tbody.appendChild(tr); +} + +// ── Mutations (in production: real fetch calls) ────────────────────────────── + +function revokeKey(id) { + if (!confirm('Revoke this key? All requests using it will fail immediately.')) return; + + // Production: fetch(apiBase() + '/' + id, { method: 'PATCH', body: JSON.stringify({ status: 'revoked' }) }) + var k = KEYS.find(function(k) { return k.id === id; }); + if (k) { k.status = 'revoked'; renderTable(); } +} + +function deleteKey(id) { + // Production: fetch(apiBase() + '/' + id, { method: 'DELETE' }) + KEYS.splice(KEYS.findIndex(function(k) { return k.id === id; }), 1); + renderTable(); +} + +function createKey(name, scopes) { + // Production: fetch(apiBase(), { method: 'POST', body: JSON.stringify({ name, scopes }) }) + // → response: { key: 'mcp_...', prefix: 'mcp_...', id: '...' } + var rand = Array.from({length: 32}, function() { return Math.random().toString(36)[2]; }).join(''); + var rawKey = 'mcp_' + name.toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 4) + '_' + rand; + var prefix = rawKey.slice(0, 12) + '…'; + var newKey = { + id: 'key_' + Date.now(), + name: name, + prefix: prefix, + scopes: scopes, + created: new Date().toISOString().slice(0, 10), + lastUsed: 'Never', + status: 'active', + }; + KEYS.unshift(newKey); + return rawKey; +} + +// ── Render ─────────────────────────────────────────────────────────────────── + +function renderScopes() { + var el = document.getElementById('scope-checkboxes'); + var tmpl = document.getElementById('tmpl-scope-checkbox'); + el.innerHTML = ''; + SCOPES.forEach(function(s) { + var node = tmpl.content.cloneNode(true); + var input = node.querySelector('input'); + input.dataset.scope = s; + if (selectedScopes.has(s)) input.checked = true; + node.querySelector('.scope-checkbox-name').textContent = s; + node.querySelector('.scope-checkbox-desc').textContent = SCOPE_DESC[s]; + input.addEventListener('change', function() { + if (this.checked) selectedScopes.add(this.dataset.scope); + else selectedScopes.delete(this.dataset.scope); + }); + el.appendChild(node); + }); +} + +function renderTable() { + var q = search.toLowerCase(); + var tbody = document.getElementById('keys-tbody'); + var rows = KEYS.filter(function(k) { + return !q || k.name.toLowerCase().includes(q) || k.prefix.includes(q); + }); + + tbody.innerHTML = ''; + + if (!rows.length) { + var empty = document.createElement('tr'); + var td = document.createElement('td'); + td.colSpan = 7; + td.style.cssText = 'text-align:center;padding:36px;color:var(--text-muted);'; + td.textContent = KEYS.length ? 'No keys match your search' : 'No API keys yet'; + empty.appendChild(td); + tbody.appendChild(empty); + return; + } + + var tmpl = document.getElementById('tmpl-key-row'); + rows.forEach(function(k) { + var node = tmpl.content.cloneNode(true); + node.querySelector('.col-name').textContent = k.name; + node.querySelector('.col-mono').textContent = k.prefix; + node.querySelector('.col-created').textContent = k.created; + node.querySelector('.col-last-used').textContent = k.lastUsed; + + var scopesEl = node.querySelector('.col-scopes'); + k.scopes.forEach(function(s) { + var span = document.createElement('span'); + span.className = 'badge badge-scope'; + span.textContent = s; + scopesEl.appendChild(span); + }); + + var badge = document.createElement('span'); + badge.className = k.status === 'active' ? 'badge badge-active' : 'badge badge-revoked'; + badge.textContent = k.status === 'active' ? 'Active' : 'Revoked'; + node.querySelector('.col-status').appendChild(badge); + + var actionsActive = node.querySelector('.actions-active'); + var actionsRevoked = node.querySelector('.actions-revoked'); + if (k.status === 'active') { + actionsActive.querySelector('[title="Copy key prefix"]') + .addEventListener('click', function() { copyPrefix(k.prefix); }); + actionsActive.querySelector('[title="Revoke key"]') + .addEventListener('click', function() { revokeKey(k.id); }); + } else { + actionsActive.style.display = 'none'; + actionsRevoked.style.display = ''; + actionsRevoked.querySelector('[title="Delete"]') + .addEventListener('click', function() { deleteKey(k.id); }); + } + + tbody.appendChild(node); + }); +} + +// ── Modal ──────────────────────────────────────────────────────────────────── + +var modal = document.getElementById('modal-create'); + +function openModal() { + document.getElementById('modal-form-body').style.display = ''; + document.getElementById('modal-reveal-body').style.display = 'none'; + document.getElementById('modal-footer-create').style.display = ''; + document.getElementById('modal-footer-done').style.display = 'none'; + document.getElementById('new-key-name').value = ''; + selectedScopes = new Set(['read']); + renderScopes(); + modal.classList.add('open'); + setTimeout(function() { document.getElementById('new-key-name').focus(); }, 50); +} + +function closeModal() { modal.classList.remove('open'); } + +document.getElementById('btn-create-key').addEventListener('click', openModal); +document.getElementById('modal-close-btn').addEventListener('click', closeModal); +document.getElementById('modal-cancel-btn').addEventListener('click', closeModal); +modal.addEventListener('click', function(e) { if (e.target === modal) closeModal(); }); + +document.getElementById('modal-confirm-btn').addEventListener('click', function() { + var name = document.getElementById('new-key-name').value.trim(); + if (!name) { document.getElementById('new-key-name').focus(); return; } + if (!selectedScopes.size) { alert('Select at least one scope.'); return; } + + var rawKey = createKey(name, Array.from(selectedScopes)); + + document.getElementById('reveal-key-value').textContent = rawKey; + document.getElementById('modal-form-body').style.display = 'none'; + document.getElementById('modal-reveal-body').style.display = ''; + document.getElementById('modal-footer-create').style.display = 'none'; + document.getElementById('modal-footer-done').style.display = ''; + renderTable(); +}); + +document.getElementById('modal-done-btn').addEventListener('click', closeModal); + +document.getElementById('copy-key-btn').addEventListener('click', function() { + var val = document.getElementById('reveal-key-value').textContent; + navigator.clipboard && navigator.clipboard.writeText(val); + this.innerHTML = ''; +}); + +document.getElementById('key-search').addEventListener('input', function() { + search = this.value; + renderTable(); +}); + +// ── Init ───────────────────────────────────────────────────────────────────── + +function copyPrefix(prefix) { + navigator.clipboard && navigator.clipboard.writeText(prefix); +} + +document.addEventListener('DOMContentLoaded', loadKeys); diff --git a/apps/ui/js/catalog.js b/apps/ui/js/catalog.js new file mode 100644 index 0000000..c9bd37c --- /dev/null +++ b/apps/ui/js/catalog.js @@ -0,0 +1,333 @@ +const PAGE_SIZE_DESKTOP = 6; +const PAGE_SIZE_MOBILE = 4; +const MOBILE_BREAKPOINT = 960; + +function getSortOptions() { + return [ + { value: 'created_desc', label: (window.t && t('sort.created_desc')) || 'Sort: Last created' }, + { value: 'created_asc', label: (window.t && t('sort.created_asc')) || 'Sort: Oldest first' }, + { value: 'name_asc', label: (window.t && t('sort.name_asc')) || 'Sort: Name A–Z' }, + { value: 'name_desc', label: (window.t && t('sort.name_desc')) || 'Sort: Name Z–A' }, + ]; +} + +function getTabDefs() { + return [ + { id: 'all', label: (window.t && t('tab.all')) || 'All' }, + { id: 'active', label: (window.t && t('tab.active')) || 'Active' }, + { id: 'draft', label: (window.t && t('tab.draft')) || 'Draft' }, + { id: 'error', label: (window.t && t('tab.error')) || 'Error' }, + { id: 'inactive', label: (window.t && t('tab.inactive')) || 'Inactive' }, + ]; +} + +document.addEventListener('alpine:init', () => { + + Alpine.data('catalog', () => ({ + // ── raw data ── + operations: [], + agents: [], + loading: true, + + // ── page size (responsive) ── + pageSize: PAGE_SIZE_DESKTOP, + + // ── filter state ── + search: '', + tab: 'all', + filterProtocol: null, + filterCategory: null, + filterAgent: null, + sort: 'created_desc', + page: 1, + + // ── dropdown open state ── + openDropdown: null, + + // ── mobile nav ── + navOpen: false, + + // ── derived option lists (built after data loads) ── + categoryOptions: [], + + async init() { + const [opsRes, agentsRes] = await Promise.all([ + fetch((window.DATA_URL||'data/')+'operations.json'), + fetch((window.DATA_URL||'data/')+'agents.json'), + ]); + let ops = await opsRes.json(); + this.agents = await agentsRes.json(); + + // Apply localStorage overrides (from wizard create/edit) + try { + const overrides = JSON.parse(localStorage.getItem('crank_ops_overrides') || '[]'); + if (overrides.length) { + const existingIds = new Set(ops.map(o => o.id)); + ops = ops.map(op => { + const ov = overrides.find(o => o.id === op.id); + return ov ? Object.assign({}, op, ov) : op; + }); + overrides + .filter(o => !existingIds.has(o.id)) + .forEach(o => ops.push(o)); + } + } catch (e) {} + + this.operations = ops; + this.categoryOptions = [...new Set(this.operations.map(op => op.category))].filter(Boolean).sort(); + this.loading = false; + + // Responsive page size + const updatePageSize = () => { + this.pageSize = window.innerWidth <= MOBILE_BREAKPOINT ? PAGE_SIZE_MOBILE : PAGE_SIZE_DESKTOP; + this.page = 1; + }; + updatePageSize(); + window.addEventListener('resize', updatePageSize); + + // Re-render labels on language change + window.addEventListener('crank:langchange', () => { + this.sort = this.sort; + applyLang(); + }); + + // Close dropdowns and mobile nav on outside click + document.addEventListener('click', (e) => { + if (!e.target.closest('.filter-dropdown, .sort-dropdown, .user-menu')) { + this.openDropdown = null; + } + if (!e.target.closest('.navbar')) { + this.navOpen = false; + } + }); + }, + + // ── helpers for filtering (shared between filtered getter and tabCount) ── + _applyNonStatusFilters(ops) { + if (this.search.trim()) { + const q = this.search.toLowerCase(); + ops = ops.filter(op => + op.name.toLowerCase().includes(q) || + op.display_name.toLowerCase().includes(q) || + (op.target_url || '').toLowerCase().includes(q) + ); + } + if (this.filterProtocol) { + ops = ops.filter(op => op.protocol === this.filterProtocol); + } + if (this.filterCategory) { + ops = ops.filter(op => op.category === this.filterCategory); + } + if (this.filterAgent) { + const agent = this.agents.find(a => a.id === this.filterAgent); + const ids = agent ? agent.operation_ids : []; + ops = ops.filter(op => ids.includes(op.id)); + } + return ops; + }, + + // ── computed: filtered + sorted + paginated ── + get filtered() { + let ops = this._applyNonStatusFilters(this.operations); + + if (this.tab !== 'all') { + ops = ops.filter(op => op.status === this.tab); + } + + ops = [...ops].sort((a, b) => { + switch (this.sort) { + case 'created_desc': return new Date(b.created_at) - new Date(a.created_at); + case 'created_asc': return new Date(a.created_at) - new Date(b.created_at); + case 'name_asc': return a.name.localeCompare(b.name); + case 'name_desc': return b.name.localeCompare(a.name); + default: return 0; + } + }); + + return ops; + }, + + get totalFiltered() { return this.filtered.length; }, + get totalPages() { return Math.max(1, Math.ceil(this.totalFiltered / this.pageSize)); }, + + get paginated() { + const start = (this.page - 1) * this.pageSize; + return this.filtered.slice(start, start + this.pageSize); + }, + + get pageStart() { return this.totalFiltered === 0 ? 0 : (this.page - 1) * this.pageSize + 1; }, + get pageEnd() { return Math.min(this.page * this.pageSize, this.totalFiltered); }, + + // ── tab counts (respect non-status filters so numbers are consistent) ── + tabCount(tab) { + const ops = this._applyNonStatusFilters(this.operations); + if (tab === 'all') return ops.length; + return ops.filter(op => op.status === tab).length; + }, + + // ── active filter chips (for dismissible chip row) ── + get activeChips() { + const chips = []; + if (this.filterProtocol) { + chips.push({ key: 'protocol', label: 'Protocol: ' + this.filterProtocol.toUpperCase() }); + } + if (this.filterCategory) { + chips.push({ key: 'category', label: 'Category: ' + this.filterCategory }); + } + if (this.filterAgent) { + const a = this.agents.find(ag => ag.id === this.filterAgent); + chips.push({ key: 'agent', label: 'Agent: ' + (a ? a.display_name : '') }); + } + return chips; + }, + + get hasActiveFilters() { + return !!(this.filterProtocol || this.filterCategory || this.filterAgent || this.search.trim()); + }, + + clearChip(key) { + if (key === 'protocol') this.filterProtocol = null; + if (key === 'category') this.filterCategory = null; + if (key === 'agent') this.filterAgent = null; + this.page = 1; + }, + + clearAllFilters() { + this.filterProtocol = null; + this.filterCategory = null; + this.filterAgent = null; + this.search = ''; + this.tab = 'all'; + this.page = 1; + }, + + // ── agent lookup per operation (for badges in rows) ── + get agentsByOpId() { + const map = {}; + this.agents.forEach(a => { + a.operation_ids.forEach(id => { + if (!map[id]) map[id] = []; + map[id].push(a); + }); + }); + return map; + }, + + // ── active agents only (for filter dropdown) ── + get activeAgents() { + return this.agents.filter(a => a.status === 'active'); + }, + + // ── sort label ── + get sortLabel() { + return getSortOptions().find(o => o.value === this.sort)?.label ?? 'Sort'; + }, + + // ── actions ── + setTab(tab) { + this.tab = tab; + this.page = 1; + }, + + setSearch(val) { + this.search = val; + this.page = 1; + }, + + setProtocol(val) { + this.filterProtocol = this.filterProtocol === val ? null : val; + this.openDropdown = null; + this.page = 1; + }, + + setCategory(val) { + this.filterCategory = this.filterCategory === val ? null : val; + this.openDropdown = null; + this.page = 1; + }, + + setSort(val) { + this.sort = val; + this.openDropdown = null; + }, + + setAgent(val) { + this.filterAgent = this.filterAgent === val ? null : val; + this.openDropdown = null; + this.page = 1; + }, + + clearProtocol() { this.filterProtocol = null; this.openDropdown = null; this.page = 1; }, + clearCategory() { this.filterCategory = null; this.openDropdown = null; this.page = 1; }, + clearAgent() { this.filterAgent = null; this.openDropdown = null; this.page = 1; }, + + toggleDropdown(name) { + this.openDropdown = this.openDropdown === name ? null : name; + }, + + goPage(p) { + if (p >= 1 && p <= this.totalPages) this.page = p; + }, + + // ── operation CRUD ── + editOperation(op) { + try { sessionStorage.setItem('wizard_edit', JSON.stringify(op)); } catch (e) {} + window.location.href = (window.APP_BASE||'')+'html/wizard/?mode=edit'; + }, + + deleteOperation(id) { + if (!confirm('Delete this operation? This cannot be undone.')) return; + this.operations = this.operations.filter(op => op.id !== id); + // Persist deletion in overrides as a tombstone + try { + const overrides = JSON.parse(localStorage.getItem('crank_ops_overrides') || '[]'); + const filtered = overrides.filter(o => o.id !== id); + filtered.push({ id, _deleted: true }); + localStorage.setItem('crank_ops_overrides', JSON.stringify(filtered)); + } catch (e) {} + }, + + // ── helpers для шаблона ── + protocolLabel(op) { + if (op.protocol === 'rest') return `REST · ${op.method}`; + if (op.protocol === 'graphql') return 'GraphQL'; + return 'gRPC'; + }, + + protocolClass(op) { + return `badge badge-${op.protocol}`; + }, + + statusClass(op) { + return `status-badge status-${op.status}`; + }, + + statusLabel(op) { + return op.status.charAt(0).toUpperCase() + op.status.slice(1); + }, + + formatDate(dateStr) { + if (!dateStr) return '—'; + return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); + }, + + truncateUrl(url) { + if (!url) return ''; + return url.length > 42 ? url.slice(0, 42) + '…' : url; + }, + + get sortOptions() { return getSortOptions(); }, + get tabDefs() { return getTabDefs(); }, + + handleNewOperation() { + sessionStorage.removeItem('wizard_edit'); + window.location.href = (window.APP_BASE||'')+'html/wizard/'; + }, + + handleLogout() { + localStorage.removeItem('crank_user'); + window.location.href = (window.APP_BASE||'')+'html/login.html'; + }, + })); + +}); diff --git a/apps/ui/js/config.js b/apps/ui/js/config.js new file mode 100644 index 0000000..6021737 --- /dev/null +++ b/apps/ui/js/config.js @@ -0,0 +1,20 @@ +/** + * config.js — auto-detects base paths based on current page location. + * Must be loaded before workspace.js and other navigation-using scripts. + */ +(function() { + var p = window.location.pathname; + // html/wizard/ is two levels deep from root + if (/\/html\/wizard\//.test(p)) { + window.APP_BASE = '../../'; + window.DATA_URL = '../../data/'; + // html/*.html is one level deep from root + } else if (/\/html\//.test(p)) { + window.APP_BASE = '../'; + window.DATA_URL = '../data/'; + // root level (index.html) + } else { + window.APP_BASE = ''; + window.DATA_URL = 'data/'; + } +}()); diff --git a/apps/ui/js/i18n.js b/apps/ui/js/i18n.js new file mode 100644 index 0000000..bef0ad1 --- /dev/null +++ b/apps/ui/js/i18n.js @@ -0,0 +1,289 @@ +/* ═══════════════════════════════════════════════ + Crank — i18n + Reads `crank_lang` from localStorage ('en'|'ru') + ═══════════════════════════════════════════════ */ + +var TRANSLATIONS = { + en: { + // Nav + 'nav.operations': 'Operations', + 'nav.agents': 'Agents', + 'nav.apikeys': 'API Keys', + 'nav.logs': 'Logs', + 'nav.usage': 'Usage', + 'nav.settings': 'Settings', + 'nav.logout': 'Log out', + + // Operations page + 'ops.title': 'Operations', + 'ops.subtitle': 'Tool contracts that expose your upstream APIs as MCP tools', + 'ops.new': 'New operation', + 'ops.loading': 'Loading operations…', + 'ops.empty.title': 'No operations found', + 'ops.empty.sub': 'Try adjusting your search or filters.', + + // Stats + 'stats.total': 'Total operations', + 'stats.active': 'Active', + 'stats.requests': 'Requests today', + 'stats.latency': 'Avg. latency', + + // Tabs + 'tab.all': 'All', + 'tab.active': 'Active', + 'tab.draft': 'Draft', + 'tab.error': 'Error', + 'tab.inactive': 'Inactive', + + // Filters & sort + 'filter.search': 'Search operations...', + 'filter.protocol': 'Protocol', + 'filter.status': 'Status', + 'filter.category': 'Category', + 'filter.showing': 'Showing', + 'filter.of': 'of', + 'filter.clear': 'Clear filter', + 'sort.created_desc':'Sort: Last created', + 'sort.created_asc': 'Sort: Oldest first', + 'sort.name_asc': 'Sort: Name A–Z', + 'sort.name_desc': 'Sort: Name Z–A', + + // Table headers + 'th.name': 'NAME', + 'th.protocol': 'PROTOCOL', + 'th.status': 'STATUS', + 'th.created': 'CREATED', + 'th.url': 'TARGET URL', + + // Pagination + 'page.showing': 'Showing', + 'page.of': 'of', + 'page.ops': 'operations', + + // API Keys page + 'apikeys.title': 'API Keys', + 'apikeys.subtitle': 'Manage access tokens for external integrations', + 'apikeys.new': 'Create key', + 'apikeys.th.name': 'NAME', + 'apikeys.th.scope': 'SCOPE', + 'apikeys.th.created':'CREATED', + 'apikeys.th.last': 'LAST USED', + + // Logs page + 'logs.title': 'Logs', + 'logs.subtitle': 'Real-time request and event log', + 'logs.search': 'Search logs...', + 'logs.level.all': 'All', + 'logs.level.info': 'Info', + 'logs.level.warn': 'Warn', + 'logs.level.error': 'Error', + 'logs.level.debug': 'Debug', + + // Usage page + 'usage.title': 'Usage', + 'usage.subtitle': 'Request volume and performance metrics', + + // Settings sidebar + 'settings.title': 'Settings', + 'settings.nav.workspace': 'Workspace', + 'settings.nav.profile': 'Profile', + 'settings.nav.members': 'Members', + 'settings.nav.security': 'Security', + 'settings.nav.notif': 'Notifications', + 'settings.nav.danger': 'Danger zone', + + // Settings — workspace section + 'settings.ws.title': 'Workspace settings', + 'settings.ws.subtitle': 'General information about your workspace', + 'settings.ws.name': 'Workspace name', + 'settings.ws.display': 'Display name', + 'settings.ws.desc': 'Description', + 'settings.ws.tz': 'Timezone', + 'settings.ws.protocol': 'Default protocol', + + // Settings — language + 'settings.lang.title': 'Language', + 'settings.lang.subtitle': 'Interface display language', + 'settings.lang.en': 'English', + 'settings.lang.ru': 'Русский', + + // Settings — profile section + 'settings.profile.title': 'Profile', + 'settings.profile.name': 'Full name', + 'settings.profile.email': 'Email', + 'settings.profile.role': 'Role', + + // Common buttons + 'btn.save': 'Save changes', + 'btn.cancel': 'Cancel', + 'btn.create': 'Create', + 'btn.delete': 'Delete', + 'btn.revoke': 'Revoke', + 'btn.copy': 'Copy', + + // Login + 'login.title': 'Sign in', + 'login.subtitle': 'Welcome back to your workspace', + 'login.email': 'Email', + 'login.password': 'Password', + 'login.submit': 'Sign in', + 'login.sso': 'Continue with Google', + }, + + ru: { + // Nav + 'nav.operations': 'Операции', + 'nav.agents': 'Агенты', + 'nav.apikeys': 'API Ключи', + 'nav.logs': 'Логи', + 'nav.usage': 'Использование', + 'nav.settings': 'Настройки', + 'nav.logout': 'Выйти', + + // Operations page + 'ops.title': 'Операции', + 'ops.subtitle': 'Контракты инструментов, открывающие ваши API как MCP-инструменты', + 'ops.new': 'Новая операция', + 'ops.loading': 'Загрузка операций…', + 'ops.empty.title': 'Операции не найдены', + 'ops.empty.sub': 'Попробуйте изменить поисковый запрос или фильтры.', + + // Stats + 'stats.total': 'Всего операций', + 'stats.active': 'Активные', + 'stats.requests': 'Запросов сегодня', + 'stats.latency': 'Ср. задержка', + + // Tabs + 'tab.all': 'Все', + 'tab.active': 'Активные', + 'tab.draft': 'Черновик', + 'tab.error': 'Ошибка', + 'tab.inactive': 'Неактивные', + + // Filters & sort + 'filter.search': 'Поиск операций...', + 'filter.protocol': 'Протокол', + 'filter.status': 'Статус', + 'filter.category': 'Категория', + 'filter.showing': 'Показано', + 'filter.of': 'из', + 'filter.clear': 'Сбросить', + 'sort.created_desc':'Сначала новые', + 'sort.created_asc': 'Сначала старые', + 'sort.name_asc': 'Название А–Я', + 'sort.name_desc': 'Название Я–А', + + // Table headers + 'th.name': 'НАЗВАНИЕ', + 'th.protocol': 'ПРОТОКОЛ', + 'th.status': 'СТАТУС', + 'th.created': 'СОЗДАН', + 'th.url': 'ЦЕЛЕВОЙ URL', + + // Pagination + 'page.showing': 'Показано', + 'page.of': 'из', + 'page.ops': 'операций', + + // API Keys page + 'apikeys.title': 'API Ключи', + 'apikeys.subtitle': 'Управление токенами доступа для внешних интеграций', + 'apikeys.new': 'Создать ключ', + 'apikeys.th.name': 'НАЗВАНИЕ', + 'apikeys.th.scope': 'ОБЛАСТЬ', + 'apikeys.th.created':'СОЗДАН', + 'apikeys.th.last': 'ПОСЛЕДНЕЕ ИСПОЛЬЗОВАНИЕ', + + // Logs page + 'logs.title': 'Логи', + 'logs.subtitle': 'Журнал запросов и событий в реальном времени', + 'logs.search': 'Поиск в логах...', + 'logs.level.all': 'Все', + 'logs.level.info': 'Info', + 'logs.level.warn': 'Warn', + 'logs.level.error': 'Error', + 'logs.level.debug': 'Debug', + + // Usage page + 'usage.title': 'Использование', + 'usage.subtitle': 'Объём запросов и метрики производительности', + + // Settings sidebar + 'settings.title': 'Настройки', + 'settings.nav.workspace': 'Воркспейс', + 'settings.nav.profile': 'Профиль', + 'settings.nav.members': 'Участники', + 'settings.nav.security': 'Безопасность', + 'settings.nav.notif': 'Уведомления', + 'settings.nav.danger': 'Опасная зона', + + // Settings — workspace section + 'settings.ws.title': 'Настройки воркспейса', + 'settings.ws.subtitle': 'Общая информация о вашем воркспейсе', + 'settings.ws.name': 'Имя воркспейса', + 'settings.ws.display': 'Отображаемое имя', + 'settings.ws.desc': 'Описание', + 'settings.ws.tz': 'Часовой пояс', + 'settings.ws.protocol': 'Протокол по умолчанию', + + // Settings — language + 'settings.lang.title': 'Язык', + 'settings.lang.subtitle': 'Язык отображения интерфейса', + 'settings.lang.en': 'English', + 'settings.lang.ru': 'Русский', + + // Settings — profile section + 'settings.profile.title': 'Профиль', + 'settings.profile.name': 'Полное имя', + 'settings.profile.email': 'Email', + 'settings.profile.role': 'Роль', + + // Common buttons + 'btn.save': 'Сохранить', + 'btn.cancel': 'Отмена', + 'btn.create': 'Создать', + 'btn.delete': 'Удалить', + 'btn.revoke': 'Отозвать', + 'btn.copy': 'Копировать', + + // Login + 'login.title': 'Войти', + 'login.subtitle': 'Добро пожаловать обратно', + 'login.email': 'Email', + 'login.password': 'Пароль', + 'login.submit': 'Войти', + 'login.sso': 'Войти через Google', + } +}; + +function t(key) { + var lang = localStorage.getItem('crank_lang') || 'en'; + var tr = TRANSLATIONS[lang] || TRANSLATIONS.en; + return tr[key] !== undefined ? tr[key] : (TRANSLATIONS.en[key] !== undefined ? TRANSLATIONS.en[key] : key); +} + +function applyLang() { + // text content + document.querySelectorAll('[data-i18n]').forEach(function(el) { + el.textContent = t(el.getAttribute('data-i18n')); + }); + // placeholder + document.querySelectorAll('[data-i18n-ph]').forEach(function(el) { + el.placeholder = t(el.getAttribute('data-i18n-ph')); + }); + // highlight active language button in settings + var lang = localStorage.getItem('crank_lang') || 'en'; + document.querySelectorAll('.lang-btn').forEach(function(btn) { + btn.classList.toggle('active', btn.dataset.lang === lang); + }); +} + +function setLang(lang) { + localStorage.setItem('crank_lang', lang); + applyLang(); + // Notify Alpine components to re-render reactive getters + window.dispatchEvent(new CustomEvent('crank:langchange', { detail: { lang: lang } })); +} + +document.addEventListener('DOMContentLoaded', applyLang); diff --git a/apps/ui/js/login.js b/apps/ui/js/login.js new file mode 100644 index 0000000..ca5e702 --- /dev/null +++ b/apps/ui/js/login.js @@ -0,0 +1,34 @@ +// If already logged in, redirect to home +try { + if (localStorage.getItem('crank_user')) { + window.location.replace((window.APP_BASE||'')+'index.html'); + } +} catch (e) {} + +document.getElementById('login-form').addEventListener('submit', function (e) { + e.preventDefault(); + var email = document.getElementById('email').value.trim(); + var password = document.getElementById('password').value; + var errEl = document.getElementById('login-error'); + + if (!email || !password) { + errEl.style.display = 'block'; + errEl.textContent = 'Please enter your email and password.'; + return; + } + + // Demo: any non-empty credentials succeed + errEl.style.display = 'none'; + var initials = email.split('@')[0].slice(0, 2).toUpperCase(); + var displayName = email.split('@')[0].replace(/[._]/g, ' ') + .split(' ').map(function(w) { return w.charAt(0).toUpperCase() + w.slice(1); }).join(' '); + var user = { + name: displayName, + email: email, + role: 'Admin', + workspace: 'acme-workspace', + initials: initials, + }; + try { localStorage.setItem('crank_user', JSON.stringify(user)); } catch (ex) {} + window.location.href = (window.APP_BASE||'')+'index.html'; +}); diff --git a/apps/ui/js/logs.js b/apps/ui/js/logs.js new file mode 100644 index 0000000..2ef3086 --- /dev/null +++ b/apps/ui/js/logs.js @@ -0,0 +1,487 @@ +// logs.js — Crank log viewer + +document.addEventListener('DOMContentLoaded', function () { + + // ── Helpers ──────────────────────────────────────────────────────────────── + + function minsAgo(mins) { + return new Date(Date.now() - mins * 60 * 1000); + } + + function fmtTs(date) { + var d = (date instanceof Date) ? date : new Date(date); + var pad = function (n, w) { return String(n).padStart(w || 2, '0'); }; + return ( + d.getFullYear() + '-' + + pad(d.getMonth() + 1) + '-' + + pad(d.getDate()) + ' ' + + pad(d.getHours()) + ':' + + pad(d.getMinutes()) + ':' + + pad(d.getSeconds()) + '.' + + pad(d.getMilliseconds(), 3) + ); + } + + // ── Seed log data ────────────────────────────────────────────────────────── + // Timestamps are computed relative to Date.now() so time-range filtering works. + + var LOGS = [ + { + id: 1, ts: minsAgo(2), level: 'info', op: 'create_crm_lead', + msg: 'Tool invoked — POST /v1/leads', duration: '142ms', status: 201, + detail: { + req: '{"first_name":"Alice","last_name":"Smith","email":"alice@acme.com"}', + res: '{"lead_id":"ld_9f3a","status":"new","created_at":"2026-03-27T14:32:11Z"}' + } + }, + { + id: 2, ts: minsAgo(5), level: 'info', op: 'get_user_profile', + msg: 'Tool invoked — GET /v1/users/me', duration: '38ms', status: 200, + detail: { + req: '{"user_id":"usr_0042"}', + res: '{"id":"usr_0042","name":"Alice Smith","email":"alice@acme.com","role":"admin"}' + } + }, + { + id: 3, ts: minsAgo(12), level: 'warn', op: 'send_slack_message', + msg: 'Rate limited by Slack API — retrying after back-off', duration: '1842ms', status: 429, + detail: { + req: '{"channel":"#ops","text":"Deploy finished"}', + res: '{"error":"ratelimited","retry_after":30}' + } + }, + { + id: 4, ts: minsAgo(18), level: 'info', op: 'fetch_invoice', + msg: 'Tool invoked — GET /invoices/inv_003', duration: '95ms', status: 200, + detail: { + req: '{"invoice_id":"inv_003"}', + res: '{"id":"inv_003","amount":2400,"currency":"USD","status":"paid"}' + } + }, + { + id: 5, ts: minsAgo(26), level: 'error', op: 'stream_telemetry', + msg: 'Upstream unavailable', duration: '0ms', status: 503, + detail: { + req: '{"stream_id":"st_881","since":"2026-03-27T14:00:00Z"}', + res: '503 Service Unavailable\nRetry-After: 60' + } + }, + { + id: 6, ts: minsAgo(38), level: 'info', op: 'create_crm_lead', + msg: 'Tool invoked — POST /v1/leads', duration: '167ms', status: 201, + detail: { + req: '{"first_name":"Bob","last_name":"Jones","email":"bob@example.com"}', + res: '{"lead_id":"ld_9f3b","status":"new"}' + } + }, + { + id: 7, ts: minsAgo(52), level: 'debug', op: 'get_user_profile', + msg: 'Schema validation passed — 4 fields mapped', duration: '41ms', status: 200, + detail: { + req: '{"user_id":"usr_0041"}', + res: '{"id":"usr_0041","name":"Bob Jones","email":"bob@example.com","role":"viewer"}' + } + }, + { + id: 8, ts: minsAgo(75), level: 'info', op: 'list_products', + msg: 'Tool invoked — GET /api/products', duration: '210ms', status: 200, + detail: { + req: '{"limit":20,"page":1}', + res: '{"total":142,"items":[{"id":"p01","name":"Mechanical Keyboard","price":149.99},...]}' + } + }, + { + id: 9, ts: minsAgo(110), level: 'info', op: 'create_support_ticket', + msg: 'Tool invoked — POST /v1/tickets', duration: '178ms', status: 201, + detail: { + req: '{"subject":"Login issue","priority":"high","user_id":"usr_0042"}', + res: '{"ticket_id":"tkt_5521","status":"open","created_at":"2026-03-27T12:42:11Z"}' + } + }, + { + id: 10, ts: minsAgo(190), level: 'error', op: 'send_slack_message', + msg: 'Upstream returned 503 Service Unavailable — retried 3×', duration: '0ms', status: 503, + detail: { + req: '{"channel":"#alerts","text":"DB failover triggered"}', + res: '503 Service Unavailable\nRetry-After: 30' + } + }, + { + id: 11, ts: minsAgo(280), level: 'info', op: 'fetch_invoice', + msg: 'Tool invoked — GET /invoices/inv_002', duration: '88ms', status: 200, + detail: { + req: '{"invoice_id":"inv_002"}', + res: '{"id":"inv_002","amount":1250,"currency":"USD","status":"open"}' + } + }, + { + id: 12, ts: minsAgo(420), level: 'warn', op: 'get_order_status', + msg: 'Validation error: missing field order_id', duration: '55ms', status: 422, + detail: { + req: '{"customer_id":"cus_009"}', + res: '{"error":"validation_failed","fields":["order_id"]}' + } + }, + { + id: 13, ts: minsAgo(720), level: 'info', op: 'create_crm_lead', + msg: 'Tool invoked — POST /v1/leads', duration: '155ms', status: 200, + detail: { + req: '{"first_name":"Carol","last_name":"Davis","email":"carol@co.io"}', + res: '{"lead_id":"ld_9f3c","status":"new"}' + } + }, + { + id: 14, ts: minsAgo(2200), level: 'info', op: 'sync_contacts', + msg: 'Tool invoked — POST /v1/contacts/sync', duration: '340ms', status: 200, + detail: { + req: '{"source":"hubspot","since":"2026-03-25T00:00:00Z"}', + res: '{"synced":48,"skipped":2,"errors":0}' + } + }, + { + id: 15, ts: minsAgo(7200), level: 'debug', op: 'list_products', + msg: 'Cache miss — fetching from origin', duration: '198ms', status: 200, + detail: { + req: '{"limit":10,"page":1}', + res: '{"total":142,"items":[...]}' + } + } + ]; + + // ── State ────────────────────────────────────────────────────────────────── + + var levelFilter = 'all'; + var searchText = ''; + var timeRange = '7d'; + var openId = null; + var liveMode = true; + var liveTimer = null; + var nextId = 100; + + // ── DOM refs ─────────────────────────────────────────────────────────────── + + var logList = document.getElementById('log-list'); + var logSearch = document.getElementById('log-search'); + var refreshBtn = document.getElementById('refresh-btn'); + var timeRangeSel = document.getElementById('time-range'); + var liveDot = document.querySelector('.live-dot'); + var liveLabel = document.querySelector('.live-label'); + + // ── Debug chip injection ─────────────────────────────────────────────────── + // The HTML has All/Info/Warn/Error chips. We add Debug after Error. + + var chipError = document.getElementById('chip-error'); + if (chipError && !document.getElementById('chip-debug')) { + var debugChip = document.createElement('button'); + debugChip.className = 'filter-chip'; + debugChip.setAttribute('data-level', 'debug'); + debugChip.id = 'chip-debug'; + var debugSpan = document.createElement('span'); + debugSpan.className = 'log-level debug'; + debugSpan.style.cssText = 'padding:0 4px;font-size:10px;'; + debugSpan.textContent = 'DEBUG'; + debugChip.appendChild(debugSpan); + chipError.parentNode.insertBefore(debugChip, chipError.nextSibling); + } + + // ── Time-range helpers ───────────────────────────────────────────────────── + + function rangeMs(range) { + var map = { '30m': 30, '1h': 60, '6h': 360, '24h': 1440, '7d': 10080 }; + var mins = map[range] || 10080; + return mins * 60 * 1000; + } + + // ── Render ───────────────────────────────────────────────────────────────── + + function statusClass(s) { + if (!s) return ''; + if (s < 300) return 'ok'; + if (s < 500) return 'warn'; + return 'err'; + } + + function escText(str) { + // Safely return string; all user-facing dynamic strings go through textContent, + // but for pre blocks in detail we use textContent assignment too (see below). + return str; + } + + function renderLogs() { + var q = searchText.toLowerCase(); + var cutoff = Date.now() - rangeMs(timeRange); + + var rows = LOGS.filter(function (l) { + var ts = (l.ts instanceof Date) ? l.ts.getTime() : new Date(l.ts).getTime(); + var matchTime = ts >= cutoff; + var matchLevel = levelFilter === 'all' || l.level === levelFilter; + var matchSearch = !q || + l.msg.toLowerCase().indexOf(q) !== -1 || + l.op.indexOf(q) !== -1; + return matchTime && matchLevel && matchSearch; + }); + + // Build DOM nodes instead of innerHTML for XSS safety where data is dynamic. + // For static/controlled detail payloads we still use textContent on pre elements. + var frag = document.createDocumentFragment(); + + if (!rows.length) { + var empty = document.createElement('div'); + empty.style.cssText = 'text-align:center;padding:40px;color:var(--text-muted);font-size:13.5px;'; + empty.textContent = 'No log entries match the current filter'; + frag.appendChild(empty); + } + + rows.forEach(function (l) { + var tsDate = (l.ts instanceof Date) ? l.ts : new Date(l.ts); + var timeStr = fmtTs(tsDate).slice(11, 23); // HH:MM:SS.mmm + + // ── Row ── + var row = document.createElement('div'); + row.className = 'log-entry'; + row.setAttribute('data-id', l.id); + + var timeEl = document.createElement('span'); + timeEl.className = 'log-time'; + timeEl.textContent = timeStr; + row.appendChild(timeEl); + + var levelEl = document.createElement('span'); + levelEl.className = 'log-level ' + l.level; + levelEl.textContent = l.level.toUpperCase(); + row.appendChild(levelEl); + + var msgDiv = document.createElement('div'); + msgDiv.className = 'log-msg'; + + var opSpan = document.createElement('span'); + opSpan.className = 'log-op-name'; + opSpan.textContent = l.op; + msgDiv.appendChild(opSpan); + + msgDiv.appendChild(document.createTextNode('\u00a0\u00a0')); // two nbsps + + if (l.status) { + var statusEl = document.createElement('span'); + statusEl.className = 'log-status ' + statusClass(l.status); + statusEl.textContent = l.status; + msgDiv.appendChild(statusEl); + msgDiv.appendChild(document.createTextNode(' \u00b7 ')); // · separator + } + + var msgText = document.createTextNode(l.msg); + msgDiv.appendChild(msgText); + row.appendChild(msgDiv); + + var durEl = document.createElement('span'); + var durSlow = (l.duration.indexOf('s') !== -1 && parseFloat(l.duration) > 1); + var durClass = durSlow ? 'slow' : (l.level === 'error' ? 'error' : ''); + durEl.className = 'log-duration' + (durClass ? ' ' + durClass : ''); + durEl.textContent = l.duration; + row.appendChild(durEl); + + frag.appendChild(row); + + // ── Expanded detail ── + if (l.detail) { + var expDiv = document.createElement('div'); + expDiv.className = 'log-entry-expanded' + (l.id === openId ? ' open' : ''); + expDiv.setAttribute('data-exp', l.id); + + if (l.detail.req) { + var reqLabel = document.createElement('div'); + reqLabel.className = 'log-detail-label'; + reqLabel.textContent = 'Request body'; + expDiv.appendChild(reqLabel); + + var reqPre = document.createElement('pre'); + reqPre.className = 'log-detail-block'; + reqPre.textContent = l.detail.req; + expDiv.appendChild(reqPre); + } + + if (l.detail.res) { + var resLabel = document.createElement('div'); + resLabel.className = 'log-detail-label'; + resLabel.textContent = 'Response'; + expDiv.appendChild(resLabel); + + var resPre = document.createElement('pre'); + resPre.className = 'log-detail-block'; + resPre.textContent = l.detail.res; + expDiv.appendChild(resPre); + } + + frag.appendChild(expDiv); + } + }); + + logList.innerHTML = ''; + logList.appendChild(frag); + + // Attach click handlers for expand/collapse + logList.querySelectorAll('.log-entry').forEach(function (el) { + el.addEventListener('click', function () { + var id = parseInt(this.getAttribute('data-id'), 10); + openId = (openId === id) ? null : id; + logList.querySelectorAll('.log-entry-expanded').forEach(function (exp) { + var expId = parseInt(exp.getAttribute('data-exp'), 10); + exp.classList.toggle('open', expId === openId); + }); + }); + }); + } + + // ── Level chip wiring ────────────────────────────────────────────────────── + + function wireChips() { + document.querySelectorAll('.filter-chip[data-level]').forEach(function (btn) { + btn.addEventListener('click', function () { + levelFilter = this.getAttribute('data-level'); + document.querySelectorAll('.filter-chip[data-level]').forEach(function (b) { + b.classList.remove('active'); + }); + this.classList.add('active'); + renderLogs(); + }); + }); + } + + wireChips(); + + // ── Search ───────────────────────────────────────────────────────────────── + + logSearch.addEventListener('input', function () { + searchText = this.value; + renderLogs(); + }); + + // ── Refresh ──────────────────────────────────────────────────────────────── + + refreshBtn.addEventListener('click', renderLogs); + + // ── Time-range select ────────────────────────────────────────────────────── + // The HTML uses a ' + + '' + + ''; + rows.appendChild(div); +} + +function removeInviteRow(btn) { + btn.closest('.invite-row').remove(); +} + +// Members management (edit mode) +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) { + console.log('Role updated:', email, '->', select.value); +} + +function removeMember(btn, name) { + if (!confirm('Remove ' + name + ' from this workspace?')) return; + btn.closest('.member-row').remove(); + 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' : ''); +} + +function revokeInvite(btn) { + btn.closest('.member-row').remove(); + var pending = document.getElementById('pending-invites'); + if (pending && !pending.querySelector('.member-row')) pending.style.display = 'none'; +} + +document.addEventListener('DOMContentLoaded', function() { + initPage(); + if (isCreateMode) document.getElementById('ws-name').focus(); +}); + +window.addEventListener('beforeunload', function(e) { + if (formDirty) { + e.preventDefault(); + e.returnValue = ''; + } +}); diff --git a/apps/ui/js/workspace.js b/apps/ui/js/workspace.js new file mode 100644 index 0000000..833da4a --- /dev/null +++ b/apps/ui/js/workspace.js @@ -0,0 +1,64 @@ +/* ═══════════════════════════════════════════════════ + Crank — workspace switcher + ═══════════════════════════════════════════════════ */ + +var WS_LIST = [ + { slug: 'acme-workspace', name: 'acme-workspace', role: 'Owner', letter: 'A', color: '#0d9488' }, + { slug: 'startup-demo', name: 'startup-demo', role: 'Developer', letter: 'S', color: '#7c3aed' }, + { slug: 'personal', name: 'personal', role: 'Owner', letter: 'P', color: '#0891b2' }, +]; + +function getCurrentWs() { + var slug = localStorage.getItem('crank_workspace') || 'acme-workspace'; + return WS_LIST.find(function(w) { return w.slug === slug; }) || WS_LIST[0]; +} + +function initWorkspaceSwitcher() { + var ws = getCurrentWs(); + var nameEl = document.getElementById('ws-current-name'); + var dotEl = document.getElementById('ws-dot'); + if (nameEl) nameEl.textContent = ws.name; + if (dotEl) { dotEl.textContent = ws.letter; dotEl.style.background = ws.color; } + + var list = document.getElementById('ws-dropdown-list'); + if (!list) return; + + list.innerHTML = WS_LIST.map(function(w) { + var active = w.slug === ws.slug; + return '
' + + '
' + w.letter + '
' + + '
' + + '
' + w.name + '
' + + '
' + w.role + '
' + + '
' + + (active ? '' : '') + + '
'; + }).join('') + + '
' + + '' + + '' + + 'Workspace settings' + + ''; +} + +function toggleWsSwitcher(e) { + e.stopPropagation(); + var dd = document.getElementById('ws-dropdown'); + if (dd) dd.style.display = dd.style.display === 'none' ? '' : 'none'; +} + +function switchWorkspace(slug) { + localStorage.setItem('crank_workspace', slug); + var dd = document.getElementById('ws-dropdown'); + if (dd) dd.style.display = 'none'; + initWorkspaceSwitcher(); +} + +document.addEventListener('DOMContentLoaded', initWorkspaceSwitcher); + +document.addEventListener('click', function(e) { + if (!e.target.closest('#ws-switcher')) { + var dd = document.getElementById('ws-dropdown'); + if (dd) dd.style.display = 'none'; + } +});