From 7aabf7077a0dc8c3de1ed641dd7f073b96245a8a Mon Sep 17 00:00:00 2001 From: "a.tolmachev" Date: Tue, 31 Mar 2026 16:32:59 +0300 Subject: [PATCH] feat: polish alpine settings and wizard feedback --- TASKS.md | 10 +- apps/ui/css/wizard.css | 36 +++++ apps/ui/html/settings.html | 288 ++++----------------------------- apps/ui/html/wizard/step5.html | 11 ++ apps/ui/js/settings.js | 97 ++++------- apps/ui/js/wizard.js | 61 +++++-- 6 files changed, 167 insertions(+), 336 deletions(-) diff --git a/TASKS.md b/TASKS.md index e17b96b..62aeb5e 100644 --- a/TASKS.md +++ b/TASKS.md @@ -2,14 +2,14 @@ ## Current -### `feat/platform-key-usage` +### `feat/alpine-polish` -Status: completed +Status: in_progress DoD: -- platform API keys authenticate real machine access -- `last_used_at` updates from successful platform key usage -- UI API keys page reflects honest runtime usage rather than seed-only metadata +- Alpine UI pages have consistent feedback and shell state +- Settings page exposes only honest live functionality and clearly marks deferred capabilities +- Wizard live actions have stable loading and feedback behavior on real backend requests ## Next diff --git a/apps/ui/css/wizard.css b/apps/ui/css/wizard.css index 0cfa730..f99a4b3 100644 --- a/apps/ui/css/wizard.css +++ b/apps/ui/css/wizard.css @@ -654,6 +654,21 @@ border-radius: 8px; } +.info-callout.info-callout-subtle { + background: rgba(148, 163, 184, 0.08); + border-color: rgba(148, 163, 184, 0.18); +} + +.info-callout.is-success { + background: rgba(13, 148, 136, 0.07); + border-color: rgba(13, 148, 136, 0.28); +} + +.info-callout.is-error { + background: rgba(248, 113, 113, 0.08); + border-color: rgba(248, 113, 113, 0.28); +} + .info-callout-icon { width: 18px; height: 18px; @@ -662,6 +677,19 @@ color: var(--accent); } +.info-callout.info-callout-subtle .info-callout-icon { + color: var(--text-secondary); +} + +.info-callout.is-error .info-callout-icon, +.info-callout.is-error .info-callout-title { + color: #f87171; +} + +.info-callout.is-success .info-callout-title { + color: var(--accent-bright); +} + .info-callout-body { flex: 1; } .info-callout-title { @@ -1404,6 +1432,14 @@ } .btn-ghost-sm:hover { border-color: var(--text-secondary); color: var(--text-primary); } +.btn-primary-sm:disabled, +.btn-ghost-sm:disabled, +.btn-primary-sm.is-busy, +.btn-ghost-sm.is-busy { + opacity: 0.68; + cursor: wait; +} + /* ══════════════════════════════════════════════ HTTP method picker (Step 4 REST) ══════════════════════════════════════════════ */ diff --git a/apps/ui/html/settings.html b/apps/ui/html/settings.html index 118c960..b2f1663 100644 --- a/apps/ui/html/settings.html +++ b/apps/ui/html/settings.html @@ -96,7 +96,7 @@ @@ -215,10 +215,10 @@
Operator
operator@acme-workspace
-
Profile photo upload is not available yet.
+
Your avatar is generated from your display name and email.
-
Your name and email are stored on the backend and used across the console.
+
Your name and email are stored on the backend and used across the console and session identity.
@@ -245,196 +245,6 @@
- - - - -
@@ -464,8 +274,15 @@
-
Advanced security
-
Two-factor authentication, passkeys and session management are not implemented yet. The current live security flow is password-based login with HttpOnly session cookies.
+
Planned security capabilities
+
+ + + +
+ Not available in this build. Two-factor authentication, passkeys and multi-session inventory are planned, but the current live flow is password-based login with one HttpOnly browser session. +
+
@@ -477,7 +294,7 @@
Browser session — current
-
Session expires according to server-side auth settings. Use Log out to revoke the current browser session.
+
Loading current session details…
@@ -491,73 +308,28 @@
Notification preferences
-
-
-
Operation error spike
-
Alert when error rate exceeds 5% over a 5-minute window.
+
+ + + + +
+ Notifications are not wired yet. Alert routing and per-user preferences are planned, but this build does not persist or deliver notification settings.
-
-
-
-
Upstream latency degradation
-
Alert when p99 latency exceeds the baseline by 3×.
+
+
+
Operation error spike
+
Alert when error rate exceeds a rolling threshold for a published tool.
-
-
-
-
-
Quota approaching limit
-
Email when any operation reaches 80% of its monthly quota.
+
+
Upstream latency degradation
+
Notify operators when p99 latency rises sharply above the baseline.
-
-
-
-
-
New team member joined
-
Notify workspace admins when someone accepts an invite.
+
+
Quota and usage digests
+
Deliver periodic summaries for quota consumption, errors and workspace activity.
-
-
-
-
-
Weekly usage digest
-
Email summary of invocations, errors and latency each Monday.
-
-
-
-
Notification preferences are local UI placeholders for now and are not synced to the backend yet.
-
-
-
- - -
+
+ + + + +
+
Live actions save the draft first
+
Sample uploads, test runs, YAML actions, publish and gRPC descriptor discovery all persist the current draft before calling the backend.
+
+
+
diff --git a/apps/ui/js/settings.js b/apps/ui/js/settings.js index 31b5dce..782c114 100644 --- a/apps/ui/js/settings.js +++ b/apps/ui/js/settings.js @@ -5,16 +5,6 @@ function settingsWorkspaceId() { return workspace ? workspace.id : null; } -function downloadSettingsJson(fileName, value) { - var blob = new Blob([JSON.stringify(value, null, 2)], { type: 'application/json;charset=utf-8' }); - var url = URL.createObjectURL(blob); - var link = document.createElement('a'); - link.href = url; - link.download = fileName; - link.click(); - URL.revokeObjectURL(url); -} - function initials(displayName, email) { var source = displayName || email || 'Crank'; return source @@ -34,6 +24,37 @@ function setStatus(id, text, isError) { : 'var(--text-muted)'; } +function titleCaseRole(role) { + return String(role || 'viewer').replace(/^\w/, function(character) { + return character.toUpperCase(); + }); +} + +function populateCurrentSession(session) { + var summary = document.getElementById('settings-session-summary'); + if (!summary) { + return; + } + + if (!(session && session.user)) { + summary.textContent = 'Session details are unavailable. Use Log out to revoke the current browser session.'; + return; + } + + var membership = null; + if (Array.isArray(session.memberships)) { + membership = session.memberships.find(function(item) { + return item.workspace && item.workspace.id === session.current_workspace_id; + }) || session.memberships[0] || null; + } + + var workspaceLabel = membership && membership.workspace + ? (membership.workspace.display_name || membership.workspace.slug || membership.workspace.id) + : 'current workspace'; + var roleLabel = membership ? titleCaseRole(membership.role) : 'Viewer'; + summary.textContent = session.user.email + ' · ' + roleLabel + ' in ' + workspaceLabel + '. Use Log out to revoke the current browser session.'; +} + function populateProfile(session) { if (!session || !session.user) { return; @@ -52,6 +73,8 @@ function populateProfile(session) { if (email) { email.value = user.email || ''; } + + populateCurrentSession(session); } async function loadProfile() { @@ -285,65 +308,11 @@ async function loadWorkspaceSettings() { } } -function bindDangerZoneActions() { - var renameButton = document.getElementById('settings-ws-rename-btn'); - var exportButton = document.getElementById('settings-ws-export-btn'); - var deleteButton = document.getElementById('settings-ws-delete-btn'); - - if (renameButton && renameButton.dataset.bound !== 'true') { - renameButton.dataset.bound = 'true'; - renameButton.addEventListener('click', function() { - document.getElementById('settings-ws-slug').focus(); - if (window.CrankUi) { - window.CrankUi.info('Update the workspace slug above and save changes to rename it.', 'Rename workspace'); - } - }); - } - - if (exportButton && exportButton.dataset.bound !== 'true') { - exportButton.dataset.bound = 'true'; - exportButton.addEventListener('click', async function() { - var workspaceId = settingsWorkspaceId(); - if (!workspaceId || !window.CrankApi) return; - try { - var snapshot = await window.CrankApi.exportWorkspace(workspaceId); - var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null; - downloadSettingsJson((workspace ? workspace.slug : 'workspace') + '-snapshot.json', snapshot); - if (window.CrankUi) { - window.CrankUi.success('Workspace export downloaded.', 'Export complete'); - } - } catch (error) { - if (window.CrankUi) { - window.CrankUi.error(error.message || 'Failed to export workspace', 'Export failed'); - } - } - }); - } - - if (deleteButton && deleteButton.dataset.bound !== 'true') { - deleteButton.dataset.bound = 'true'; - deleteButton.addEventListener('click', async function() { - var workspaceId = settingsWorkspaceId(); - if (!workspaceId || !window.CrankApi || !window.CrankAuth) return; - if (!window.confirm('Delete workspace? This cannot be undone.')) return; - try { - await window.CrankApi.deleteWorkspace(workspaceId); - await window.CrankAuth.logout(); - } catch (error) { - if (window.CrankUi) { - window.CrankUi.error(error.message || 'Failed to delete workspace', 'Delete failed'); - } - } - }); - } -} - document.addEventListener('DOMContentLoaded', function () { injectLanguageSwitcher(); bindSectionNavigation(); loadProfile(); bindProfileSave(); bindPasswordSave(); - bindDangerZoneActions(); loadWorkspaceSettings(); }); diff --git a/apps/ui/js/wizard.js b/apps/ui/js/wizard.js index 5c4fb11..b82ae3e 100644 --- a/apps/ui/js/wizard.js +++ b/apps/ui/js/wizard.js @@ -1352,19 +1352,19 @@ function prefillWizardFromEdit(detail, versionDocument) { } function bindWizardLiveActions() { - bindClick('wizard-upload-input-sample', uploadInputSampleFromWizard); - bindClick('wizard-upload-output-sample', uploadOutputSampleFromWizard); - bindClick('wizard-generate-draft', generateDraftFromWizard); - bindClick('wizard-run-test', runWizardTest); + bindLiveAction('wizard-upload-input-sample', 'Saving input sample…', uploadInputSampleFromWizard); + bindLiveAction('wizard-upload-output-sample', 'Saving output sample…', uploadOutputSampleFromWizard); + bindLiveAction('wizard-generate-draft', 'Generating draft…', generateDraftFromWizard); + bindLiveAction('wizard-run-test', 'Running test…', runWizardTest); bindClick('wizard-copy-test-response', copyTestResponseToOutputSample); - bindClick('wizard-export-yaml', exportWizardYaml); - bindClick('wizard-import-yaml', importWizardYaml); - bindClick('wizard-publish-operation', publishWizardOperation); + bindLiveAction('wizard-export-yaml', 'Exporting YAML…', exportWizardYaml); + bindLiveAction('wizard-import-yaml', 'Importing YAML…', importWizardYaml); + bindLiveAction('wizard-publish-operation', 'Publishing operation…', publishWizardOperation); bindClick('wizard-select-descriptor-set', function() { var input = document.getElementById('wizard-descriptor-set-input'); if (input) input.click(); }); - bindClick('wizard-upload-descriptor-set', uploadDescriptorSetAndDiscover); + bindLiveAction('wizard-upload-descriptor-set', 'Uploading descriptor set…', uploadDescriptorSetAndDiscover); bindClick('wizard-import-yaml-file-trigger', function() { var input = document.getElementById('wizard-import-yaml-file'); if (input) input.click(); @@ -1386,6 +1386,48 @@ function bindClick(id, handler) { }); } +function bindLiveAction(id, busyLabel, handler) { + var element = document.getElementById(id); + if (!element) return; + element.addEventListener('click', function(event) { + event.preventDefault(); + runWizardLiveAction(element, busyLabel, handler); + }); +} + +async function runWizardLiveAction(button, busyLabel, handler) { + if (!button || button.dataset.busy === 'true') { + return; + } + + var originalLabel = button.textContent; + button.dataset.busy = 'true'; + button.disabled = true; + button.classList.add('is-busy'); + button.textContent = busyLabel; + + try { + await handler(); + } catch (error) { + showWizardLiveStatus( + 'Live action failed', + error && error.message ? error.message : 'The backend request failed. Review the draft and try again.', + true + ); + if (window.CrankUi) { + window.CrankUi.error( + error && error.message ? error.message : 'The backend request failed. Review the draft and try again.', + 'Wizard action failed' + ); + } + } finally { + button.dataset.busy = 'false'; + button.disabled = false; + button.classList.remove('is-busy'); + button.textContent = originalLabel; + } +} + function updateWizardProtocolVisibility() { var grpcTools = document.getElementById('wizard-grpc-live-tools'); if (grpcTools) grpcTools.style.display = wizardProtocol === 'grpc' ? '' : 'none'; @@ -1399,7 +1441,8 @@ function showWizardLiveStatus(title, text, isError) { titleEl.textContent = title; textEl.textContent = text; root.style.display = ''; - root.style.borderColor = isError ? 'rgba(248, 113, 113, 0.35)' : ''; + root.classList.toggle('is-error', !!isError); + root.classList.toggle('is-success', !isError); } function currentDraftVersion() {