From 4ee1b3e0735c8df406b8f3898a83a1edef6f819e Mon Sep 17 00:00:00 2001 From: siduck Date: Fri, 16 Oct 2026 12:15:16 +0530 Subject: [PATCH 01/17] fix(ui): theme terminal with espresso tokens --- .../src/components/common/TerminalOutput.vue | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/admin/frontend/dashboard/src/components/common/TerminalOutput.vue b/admin/frontend/dashboard/src/components/common/TerminalOutput.vue index 4ed1a8419..e22181e1a 100644 --- a/admin/frontend/dashboard/src/components/common/TerminalOutput.vue +++ b/admin/frontend/dashboard/src/components/common/TerminalOutput.vue @@ -36,13 +36,13 @@ defineExpose({ scrollToBottom }) diff --git a/admin/frontend/dashboard/src/components/navigation/Sidebar.vue b/admin/frontend/dashboard/src/components/navigation/Sidebar.vue index da3e07d95..ac2f94f08 100644 --- a/admin/frontend/dashboard/src/components/navigation/Sidebar.vue +++ b/admin/frontend/dashboard/src/components/navigation/Sidebar.vue @@ -5,6 +5,7 @@ import { Sidebar, SidebarCollapseToggle, SidebarHeader, SidebarItem, SidebarLabe import PilotLogo from '@/components/icons/Pilot.vue' import NotificationsPanel from '@/components/notifications/NotificationsPanel.vue' +import SetupChecklist from '@/components/navigation/SetupChecklist.vue' import { sidebarSections } from '@/components/navigation/list' import { useAppMenu } from '@/components/navigation/useAppMenu' @@ -36,7 +37,7 @@ const isActive = (to: string) => route.path === to || route.path.startsWith(`${t + + diff --git a/admin/frontend/dashboard/src/components/settings/S3Bucket.vue b/admin/frontend/dashboard/src/components/settings/S3Bucket.vue index 8a23e6770..ae09cc901 100644 --- a/admin/frontend/dashboard/src/components/settings/S3Bucket.vue +++ b/admin/frontend/dashboard/src/components/settings/S3Bucket.vue @@ -1,138 +1,31 @@ @@ -166,22 +59,12 @@ onMounted(load)

{{ providerLabel }} · Access key {{ accessKey }}

-
- -
+ + +
- + { + const loading = ref(true) + const connecting = ref(false) + const verifying = ref(false) + const error = ref('') + const status = ref(null) + const username = ref('') + const token = ref('') + + const viaCentral = computed(() => status.value?.auth_type === 'github_app') + const connected = computed(() => + Boolean(status.value?.connected && (viaCentral.value || status.value?.is_token_valid)), + ) + const tokenHelpUrl = computed( + () => + status.value?.providers?.github || + 'https://github.com/settings/tokens/new?scopes=repo&description=Bench+CLI', + ) + + const load = async () => { + loading.value = true + + try { + status.value = await gitApi.status() + + if (status.value?.username && !viaCentral.value) username.value = status.value.username + } finally { + loading.value = false + } + } + + const connect = async () => { + if (!token.value.trim()) { + error.value = 'Paste a personal access token to connect.' + + return false + } + + connecting.value = true + error.value = '' + + try { + const result = await gitApi.connect('github', token.value.trim(), username.value.trim()) + + if (hasApiError(result)) { + error.value = apiErrorMessage(result, 'Could not verify token.') + + return false + } + + token.value = '' + status.value = { ...result, central: status.value?.central } + toast.success(`Connected as ${result.username}`) + + return true + } catch (e) { + error.value = errorMessage(e, 'Could not verify token.') + + return false + } finally { + connecting.value = false + } + } + + const verifyConnection = async () => { + verifying.value = true + + try { + const result = await gitApi.repos() + + if (Array.isArray(result)) toast.success('GitHub connection is working') + else toast.error(apiErrorMessage(result, 'GitHub connection failed')) + } catch (e) { + toast.error(errorMessage(e, 'GitHub connection failed')) + } finally { + await load() + verifying.value = false + } + } + + const disconnect = async () => { + const response = await gitApi.disconnect() + + if (!response.ok) { + toast.error(apiErrorMessage(await response.json(), 'Could not disconnect GitHub.')) + + return + } + + username.value = '' + await load() + } + + return { + loading, + connecting, + verifying, + error, + status, + username, + token, + connected, + tokenHelpUrl, + load, + connect, + verifyConnection, + disconnect, + } +} diff --git a/admin/frontend/dashboard/src/composables/settings/useObjectStorage.ts b/admin/frontend/dashboard/src/composables/settings/useObjectStorage.ts new file mode 100644 index 000000000..b80cdcbc7 --- /dev/null +++ b/admin/frontend/dashboard/src/composables/settings/useObjectStorage.ts @@ -0,0 +1,172 @@ +import { computed, ref, watch } from 'vue' +import { toast } from 'frappe-ui' + +import { settingsApi } from '@/api/settings' +import type { S3ProviderOption } from '@/types/settings' +import { errorMessage } from '@/utils/error' + +export const useObjectStorage = () => { + const loading = ref(true) + const saving = ref(false) + const disconnecting = ref(false) + const error = ref('') + const accessKey = ref('') + const secretKey = ref('') + const bucket = ref('') + const provider = ref('') + const region = ref('') + const endpointUrl = ref('') + const secretKeySet = ref(false) + const providers = ref([]) + const loadingRegions = ref(false) + + const connected = computed(() => Boolean(accessKey.value && bucket.value && secretKeySet.value)) + const providerLabel = computed( + () => providers.value.find((p) => p.value === provider.value)?.label || provider.value, + ) + const providerOptions = computed(() => + providers.value.map((p) => ({ label: p.label, value: p.value })), + ) + const regionOptions = computed( + () => + providers.value + .find((p) => p.value === provider.value) + ?.regions.map((r) => ({ label: r, value: r })) || [], + ) + + const loadFrappeRegions = async () => { + const frappe = providers.value.find((p) => p.value === 'frappe') + + if (!frappe) return + + loadingRegions.value = true + + try { + frappe.regions = await settingsApi.frappeStorageRegions() + } catch (e) { + error.value = errorMessage(e, 'Could not read Frappe storage regions from Central.') + } finally { + loadingRegions.value = false + } + } + + watch([provider, providers], async ([current], [previous]) => { + if (previous && current !== previous) { + endpointUrl.value = '' + } + + if (current === 'frappe') { + await loadFrappeRegions() + } + + if (!regionOptions.value.some((o) => o.value === region.value)) { + region.value = regionOptions.value[0]?.value || '' + } + }) + + const canSave = computed( + () => + Boolean(accessKey.value.trim()) && + Boolean(bucket.value.trim()) && + Boolean(provider.value) && + Boolean(region.value) && + (secretKeySet.value || Boolean(secretKey.value.trim())), + ) + + const load = async () => { + loading.value = true + + try { + const data = await settingsApi.get() + const s3 = data.s3 || {} + + providers.value = data.s3_providers || [] + accessKey.value = s3.access_key || '' + bucket.value = s3.bucket || '' + provider.value = s3.provider || providers.value[0]?.value || '' + region.value = s3.region || '' + endpointUrl.value = s3.endpoint_url || '' + secretKeySet.value = !!s3.secret_key_set + } catch (e) { + error.value = errorMessage(e, 'Could not load settings.') + } finally { + loading.value = false + } + } + + const save = async () => { + saving.value = true + error.value = '' + + try { + await settingsApi.update({ + s3: { + access_key: accessKey.value.trim(), + secret_key: secretKey.value.trim(), + bucket: bucket.value.trim(), + provider: provider.value, + region: region.value, + endpoint_url: endpointUrl.value.trim(), + }, + }) + + secretKey.value = '' + toast.success('Object storage settings saved') + await load() + + return true + } catch (e) { + error.value = errorMessage(e, 'Could not save object storage settings.') + + return false + } finally { + saving.value = false + } + } + + const disconnect = async () => { + disconnecting.value = true + + try { + await settingsApi.update({ s3: { disconnect: true } }) + + accessKey.value = '' + secretKey.value = '' + bucket.value = '' + provider.value = providers.value[0]?.value || '' + region.value = '' + endpointUrl.value = '' + secretKeySet.value = false + + toast.success('Object storage disconnected') + } catch (e) { + toast.error(errorMessage(e, 'Could not disconnect object storage.')) + } finally { + disconnecting.value = false + } + } + + return { + loading, + saving, + disconnecting, + error, + accessKey, + secretKey, + bucket, + provider, + region, + endpointUrl, + secretKeySet, + providers, + loadingRegions, + connected, + providerLabel, + providerOptions, + regionOptions, + canSave, + load, + save, + disconnect, + } +} From a548a5a1b8cb3361f0bd8033848c7e3c3ed9c4ac Mon Sep 17 00:00:00 2001 From: siduck Date: Sat, 17 Oct 2026 17:42:31 +0530 Subject: [PATCH 05/17] feat(ui): prompt to connect storage on site backups --- .../src/components/sites/Backups.vue | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/admin/frontend/dashboard/src/components/sites/Backups.vue b/admin/frontend/dashboard/src/components/sites/Backups.vue index 619979416..f08fdb4ed 100644 --- a/admin/frontend/dashboard/src/components/sites/Backups.vue +++ b/admin/frontend/dashboard/src/components/sites/Backups.vue @@ -1,5 +1,14 @@