This is an automated email from the ASF dual-hosted git repository.

mgubaidullin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-karavan.git

commit 1904c9a149a79e5ab7cb21a696da47c4d1f11005
Author: Marat Gubaidullin <[email protected]>
AuthorDate: Mon Aug 24 17:54:18 2026 -0400

    Fix
---
 .../src/main/webui/src/models/CatalogModels.ts     |  86 +++
 .../src/main/webui/src/stores/DashboardStore.ts    |  60 ++
 .../main/webui/src/stores/useProjectInfoStore.ts   |  68 +++
 .../ui/page-projects/CreateProjectDrawerPanel.tsx  |  31 +
 .../src/ui/page-projects/CreateProjectPanel.tsx    | 149 +++++
 .../ui/page-projects/useCreateProjectFormUtil.tsx  | 631 +++++++++++++++++++++
 .../src/main/webui/src/ui/shared/icons/logo.svg    | 178 ++++++
 7 files changed, 1203 insertions(+)

diff --git a/karavan-app/src/main/webui/src/models/CatalogModels.ts 
b/karavan-app/src/main/webui/src/models/CatalogModels.ts
new file mode 100644
index 00000000..ecd2d087
--- /dev/null
+++ b/karavan-app/src/main/webui/src/models/CatalogModels.ts
@@ -0,0 +1,86 @@
+import {ComplexityRouteType} from "./ComplexityModels";
+
+export interface JsonSchemaProperty {
+    type?: string;
+    const?: string;
+    description?: string;
+    format?: string;
+    enum?: string[];
+    minLength?: number;
+    additionalProperties?: boolean;
+}
+
+export interface JsonSchema {
+    $schema: string;
+    $id: string;
+    title: string;
+    description?: string;
+    type: "object";
+    required?: string[];
+    properties: Record<string, JsonSchemaProperty>;
+    additionalProperties?: boolean;
+}
+
+export interface ProjectInfo {
+    projectId: string;
+    isDevModeRunning: boolean;
+    isPackagedRunning: boolean;
+    isBuildRunning: boolean;
+    routes: RouteComponentsInfo[];
+    exposesOpenApi: boolean;
+    implementsAsyncApi: boolean;
+}
+
+export interface RouteComponentsInfo {
+    routeId: string;
+    nodePrefixId: string;
+    routeTemplateRef: string;
+    type: ComplexityRouteType;
+    fileName: string;
+    consumers: ComponentInfo[];
+    producers: ComponentInfo[];
+}
+
+export interface ComponentInfo {
+    id: string;
+    name: string;
+    remote: boolean;
+    parameters: Record<string, string>;
+}
+
+export interface OperationStatistic {
+    action: string;
+    protocol: string;
+    address: string;
+    total: number;
+    inflight: number;
+    failed: number;
+    projectId?: string;
+}
+
+export const CHANNEL_PREFIX = "channel";
+export const MESSAGE_PREFIX = "message"; // fixed duplicate value
+export const TAG_PREFIX = "message"; // fixed duplicate value
+export const OPERATION_PREFIX = "operation";
+export const APPLICATION_PREFIX = "application";
+export const SERVER_PREFIX = "server";
+export const X_APPLICATIONS = "x-applications";
+export const X_APPLICATION_ID = "x-application-id";
+export const X_CAMEL_CONFIGURATION_REF = "x-camel-configuration-ref";
+export const X_CAMEL_SEND_REF = "x-camel-send-ref";
+export const X_CAMEL_RECEIVE_REF = "x-camel-receive-ref";
+export const X_CAMEL_ROUTE_REF = "x-camel-route-ref";
+export const EXTENSIONS_FIELD_NAME = "extensions";
+export const GENERATED_FILENAME_PREFIX = "_gen_";
+
+
+export const HTTP_METHODS_LOWERCASE: string[] = [
+    'get',
+    'post',
+    'put',
+    'patch',
+    'delete',
+    'head'
+]
+
+export const HTTP_METHODS: string[] = HTTP_METHODS_LOWERCASE.map(m => 
m.toUpperCase());
\ No newline at end of file
diff --git a/karavan-app/src/main/webui/src/stores/DashboardStore.ts 
b/karavan-app/src/main/webui/src/stores/DashboardStore.ts
new file mode 100644
index 00000000..f88a9682
--- /dev/null
+++ b/karavan-app/src/main/webui/src/stores/DashboardStore.ts
@@ -0,0 +1,60 @@
+import {createWithEqualityFn} from "zustand/traditional";
+import {shallow} from "zustand/shallow";
+import {Health, Metric} from "@models/DashboardModels";
+
+interface MetricState {
+    metrics: Metric[];
+    setMetrics: (metrics: Metric[]) => void;
+    updated: number;
+}
+
+export const useMetricStore = createWithEqualityFn<MetricState>((set) => ({
+    metrics: [],
+    updated: 0,
+    setMetrics: (metrics: Metric[])  => {
+        set((state: MetricState) => {
+            state.metrics.length = 0;
+            state.metrics.push(...metrics);
+            return {metrics: state.metrics, updated: Math.random()};
+        });
+    },
+}), shallow)
+
+interface HealthState {
+    healths: Health[];
+    setHealths: (healths: Health[]) => void;
+}
+
+export const useHealthStore = createWithEqualityFn<HealthState>((set) => ({
+    healths: [],
+    setHealths: (healths: Health[])  => {
+        set((state: HealthState) => {
+            state.healths.length = 0;
+            state.healths.push(...healths);
+            return {healths: state.healths};
+        });
+    },
+}), shallow)
+
+
+export type DashboardSideBarType = 'integration' | 'openAPI' | 'library' | 
'mcp'
+
+interface DashboardState {
+    showSideBar: DashboardSideBarType;
+    setShowSideBar: (showSideBar: DashboardSideBarType, title?: string) => 
void;
+    title: string;
+    setTitle: (title: string) => void;
+}
+
+export const useDashboardStore = createWithEqualityFn<DashboardState>((set) => 
({
+    showSideBar: null,
+    setShowSideBar: (showSideBar: DashboardSideBarType, title?: string) => {
+        set({ showSideBar: showSideBar, title: title });
+    },
+    title: null,
+    setTitle: (title: string) => {
+        set({ title: title });
+    },
+}), shallow)
+
+
diff --git a/karavan-app/src/main/webui/src/stores/useProjectInfoStore.ts 
b/karavan-app/src/main/webui/src/stores/useProjectInfoStore.ts
new file mode 100644
index 00000000..e62684c5
--- /dev/null
+++ b/karavan-app/src/main/webui/src/stores/useProjectInfoStore.ts
@@ -0,0 +1,68 @@
+import {BUILD_IN_PROJECTS, ContainerStatus} from "@models/ProjectModels";
+import {KaravanApi} from "@api/KaravanApi";
+import isEqual from "lodash/isEqual";
+import {ProjectInfo, RouteComponentsInfo} from "@models/CatalogModels";
+import {ComplexityApi} from "@api/ComplexityApi";
+import {ComplexityProject} from "@models/ComplexityModels";
+import {createWithEqualityFn} from "zustand/traditional";
+import {shallow} from "zustand/shallow";
+
+
+type OpenApiState = {
+    projectInfos: ProjectInfo[];
+    fetchProjectInfos: () => Promise<void>;
+}
+
+export const useOpenApiStore = createWithEqualityFn<OpenApiState>((set, get) 
=> ({
+    projectInfos: [],
+    fetchProjectInfos: async (): Promise<void> => {
+        const containerStatusesPromise = new 
Promise<ContainerStatus[]>((resolve) => {
+                KaravanApi.getAllContainerStatuses((statuses: 
ContainerStatus[]) => {
+                    resolve(statuses);
+                });
+            });
+
+        const complexitiesPromise = new Promise<ComplexityProject[]>((resolve) 
=> {
+            ComplexityApi.getComplexityProjects((complexities) => {
+                resolve(complexities);
+            });
+        });
+
+        // Wait for BOTH API calls
+        const [containers, complexities] = await Promise.all([
+            containerStatusesPromise,
+            complexitiesPromise
+        ]);
+        const currenProjectInfos = get().projectInfos;
+        const projectInfos: ProjectInfo[] = [];
+        complexities.filter(c => 
!BUILD_IN_PROJECTS.includes(c.type)).forEach(c => {
+            const routes: RouteComponentsInfo[] = []
+            c.routes.forEach(r => {
+                routes.push({
+                    routeId: r.routeId,
+                    nodePrefixId: r.nodePrefixId,
+                    fileName: r.fileName,
+                    consumers: r.consumers,
+                    producers: r.producers,
+                    routeTemplateRef: r.routeTemplateRef,
+                    type: r.type
+                })
+            })
+            const isDevModeRunning = containers.filter(cs => cs.projectId === 
c.projectId && cs.type === 'devmode')?.at(0)?.state === "running"
+            const isPackagedRunning = containers.filter(cs => cs.projectId === 
c.projectId && cs.type === 'packaged')?.at(0)?.state === "running"
+            const isBuildRunning = containers.filter(cs => cs.projectId === 
c.projectId && cs.type === 'build')?.at(0)?.state === "running"
+            projectInfos.push({
+                projectId: c.projectId,
+                isDevModeRunning: isDevModeRunning,
+                isPackagedRunning: isPackagedRunning,
+                isBuildRunning: isBuildRunning,
+                routes: routes,
+                exposesOpenApi: c.exposesOpenApi,
+            })
+        });
+
+        if (!isEqual(currenProjectInfos, projectInfos)) {
+            set({ projectInfos: projectInfos });
+        }
+    },
+}), shallow)
diff --git 
a/karavan-app/src/main/webui/src/ui/page-projects/CreateProjectDrawerPanel.tsx 
b/karavan-app/src/main/webui/src/ui/page-projects/CreateProjectDrawerPanel.tsx
new file mode 100644
index 00000000..d13d5299
--- /dev/null
+++ 
b/karavan-app/src/main/webui/src/ui/page-projects/CreateProjectDrawerPanel.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import {Button, Content, Divider, DrawerHead, DrawerPanelContent,} from 
'@patternfly/react-core';
+import {useDashboardStore} from "@stores/DashboardStore";
+import {TimesIcon} from "@patternfly/react-icons";
+import {DashboardDevelopmentProjectPanel} from 
"./DashboardDevelopmentProjectPanel";
+
+function DashboardDevelopmentDrawerPanel() {
+
+    const {setShowSideBar, showSideBar, title} = useDashboardStore();
+
+    return (
+        <DrawerPanelContent maxSize={'1000px'} defaultSize={'50%'} 
minSize={'500px'} isResizable>
+            <div style={{display: 'flex', flexDirection: 'column', height: 
'100%'}}>
+                {/* --- TOP: Fixed Header --- */}
+                <DrawerHead>
+                    <div style={{display: 'flex', flexDirection: 'row', 
alignItems: 'center', justifyContent: 'space-between', padding: '0'}}
+                         onClick={e => e.stopPropagation()}>
+                        <Content style={{flex: 1}} 
component={'h6'}>{title}</Content>
+                        <Button variant="link" icon={<TimesIcon/>} onClick={() 
=> {
+                            setShowSideBar(null);
+                        }}></Button>
+                    </div>
+                </DrawerHead>
+                <Divider style={{marginTop: 0}}/>
+                {showSideBar === 'integration' && 
<DashboardDevelopmentProjectPanel/>}
+            </div>
+        </DrawerPanelContent>
+    )
+}
+
+export default DashboardDevelopmentDrawerPanel
\ No newline at end of file
diff --git 
a/karavan-app/src/main/webui/src/ui/page-projects/CreateProjectPanel.tsx 
b/karavan-app/src/main/webui/src/ui/page-projects/CreateProjectPanel.tsx
new file mode 100644
index 00000000..11725316
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/page-projects/CreateProjectPanel.tsx
@@ -0,0 +1,149 @@
+import React, {lazy, Suspense, useEffect} from 'react';
+import {Alert, Button, Divider, DrawerPanelBody, FormAlert} from 
'@patternfly/react-core';
+import {useProjectsStore} from "@stores/ProjectStore";
+import {Project, RESERVED_WORDS} from "@models/ProjectModels";
+import {isValidProjectId, nameToProjectId} from "@utils/StringUtils";
+import {EventBus} from "@designer/utils/EventBus";
+import {useForm} from "react-hook-form";
+import {AxiosResponse} from "axios";
+import {useDashboardStore} from "@stores/DashboardStore";
+import {ProjectService} from "@services/ProjectService";
+import {SideBarFormWrapper} from "@shared/ui/SideBarFormWrapper";
+import {useProjectInfoStore} from "@stores/useProjectInfoStore";
+import {useNavigate} from "react-router-dom";
+import {ROUTES} from "@compass/navigation/Routes";
+import {ProjectFunctionHook} from "@page-project/ProjectFunctionHook";
+import {KaravanApi} from "@api/KaravanApi";
+import {useCommandPaletteStore} from "@command-palette/useCommandPaletteStore";
+import {CommandPaletteFooter} from "@command-palette/CommandPaletteFooter";
+import {DslMetaModel} from "@designer/utils/DslMetaModel";
+import {useFormUtil} from "./useFormUtil";
+
+const CommandPalettePanel = lazy(() => 
import("@command-palette/CommandPalettePanel").then(m => ({default: 
m.CommandPalettePanel})));
+
+export function DashboardDevelopmentProjectPanel() {
+
+    const projects = useProjectsStore((s) => s.projects);
+    const fetchProjectInfos = useProjectInfoStore((s) => s.fetchProjectInfos);
+    const showSideBar = useDashboardStore(state => state.showSideBar);
+    const setShowSideBar = useDashboardStore(state => state.setShowSideBar);
+    const filter = useCommandPaletteStore(state => state.filter);
+
+    const [isProjectIdChanged, setIsProjectIdChanged] = React.useState(false);
+    const [backendError, setBackendError] = React.useState<string>();
+    const {createOpenApiForProject, createRoutesForEmptyProject, 
createDlqForEmptyProject} = ProjectFunctionHook();
+    const navigate = useNavigate();
+
+    // 1. Setup Form
+    const formContext = useForm<Project>({mode: "all"});
+    const {getTextField, getCheckbox} = useFormUtil(formContext);
+    const {reset, setValue, setFocus, handleSubmit} = formContext;
+
+    // 2. Prepare Data
+    useEffect(() => {
+        if (['integration'].includes(showSideBar)) {
+            const p = new Project();
+            reset(p);
+            setBackendError(undefined);
+            setIsProjectIdChanged(false);
+            setTimeout(() => setFocus('name'), 300);
+            ProjectService.loadBlockedComponentAndKamelets();
+        }
+    }, [showSideBar, reset, setFocus]);
+
+    // 3. Save Handler
+    async function onSave(data: Project): Promise<string> {
+        await KaravanApi.postProject(data, (result, res) => after(result, res, 
data));
+        createDlqForEmptyProject(data.projectId);
+        return data.projectId;
+    }
+
+
+    function after(result: boolean, res: AxiosResponse<Project> | any, data: 
Project) {
+        if (result) {
+            EventBus.sendAlert("Success", "Project successfully created!", 
"success");
+            ProjectService.refreshProjects();
+            fetchProjectInfos();
+            setShowSideBar(null)
+            navigate(`${ROUTES.PROJECTS}/${data.projectId}`)
+        } else {
+            setBackendError(res?.response?.data);
+        }
+
+    }
+
+    // 4. Field Change Handlers
+    function onNameChange(value: string) {
+        if (!isProjectIdChanged) {
+            setValue('projectId', nameToProjectId(value), {shouldValidate: 
true});
+        }
+    }
+
+    function onIdChange(value: string) {
+        setIsProjectIdChanged(true);
+    }
+
+    const footer =
+        <div style={{display: 'flex', justifyContent: 'space-between', gap: 
6}}>
+            <Button variant={"tertiary"}
+                    onClick={() => {
+                        handleSubmit(async (data: Project) => {
+                            await onSave(data);
+                        })();
+                    }}
+            >Create Empty</Button>
+            <CommandPaletteFooter
+                onBeforeSave={(dsl: DslMetaModel) => {
+                    handleSubmit(async (data: Project) => {
+                        const projectId = await onSave(data);
+                        createRoutesForEmptyProject(dsl, projectId);
+                    })();
+                }}
+                onClose={() => setShowSideBar(null)}
+            />
+        </div>
+
+
+    return (
+        <>
+            {/* --- TOP: Fixed panel --- */}
+            <DrawerPanelBody style={{flexShrink: 0, flexGrow: 0, padding: 
'16px 16px 16px 16px'}}>
+                <SideBarFormWrapper
+                    className={"command-palette"}
+                    formContext={formContext}
+                    footer={<></>}
+                >
+
+                    {getTextField('name', 'Name', {
+                        length: v => v.length > 5 || 'Project name should be 
longer than 5 characters',
+                    }, 'text', onNameChange)}
+
+                    {getTextField('projectId', 'Project ID', {
+                        regex: v => isValidProjectId(v) || 'Only lowercase 
characters, numbers and dashes allowed',
+                        length: v => v.length > 5 || 'Project ID should be 
longer than 5 characters',
+                        name: v => !RESERVED_WORDS.includes(v) || "Reserved 
word",
+                        uniques: v => !projects.map(p => 
p.projectId).includes(v) || "Project already exists!",
+                    }, 'text', onIdChange)}
+                    {backendError && (
+                        <FormAlert>
+                            <Alert variant="danger" title={backendError} 
aria-live="polite" isInline/>
+                        </FormAlert>
+                    )}
+                </SideBarFormWrapper>
+            </DrawerPanelBody>
+
+            {/*<Divider style={{marginTop: 0}}/>*/}
+            {/* --- MIDDLE: Scrollable panel --- */}
+            <DrawerPanelBody style={{flexGrow: 1, overflowY: 'auto'}}>
+                {showSideBar === 'integration' && <Suspense 
fallback={null}><CommandPalettePanel/></Suspense>}
+            </DrawerPanelBody>
+
+            <Divider style={{marginTop: 0}}/>
+            {/* --- BOTTOM: Fixed Panel --- */}
+            {/* flexShrink: 0 prevents it from squishing. flexGrow: 0 prevents 
it from expanding. */}
+            <DrawerPanelBody style={{flexShrink: 0, flexGrow: 0, padding: 
'16px 16px 16px 16px'}}>
+                {showSideBar === 'integration' && footer}
+            </DrawerPanelBody>
+        </>
+    );
+}
\ No newline at end of file
diff --git 
a/karavan-app/src/main/webui/src/ui/page-projects/useCreateProjectFormUtil.tsx 
b/karavan-app/src/main/webui/src/ui/page-projects/useCreateProjectFormUtil.tsx
new file mode 100644
index 00000000..3acc5a5b
--- /dev/null
+++ 
b/karavan-app/src/main/webui/src/ui/page-projects/useCreateProjectFormUtil.tsx
@@ -0,0 +1,631 @@
+import React, {useState} from 'react';
+import {Controller, FieldError, UseFormReturn,} from "react-hook-form";
+import {
+    Alert,
+    Button,
+    capitalize,
+    Checkbox,
+    Content,
+    Flex,
+    FormGroup,
+    FormHelperText,
+    FormSelect,
+    FormSelectOption,
+    HelperText,
+    HelperTextItem,
+    Switch,
+    TextArea,
+    TextInput,
+    TextInputGroup,
+    TextInputGroupMain,
+    ToggleGroup,
+    ToggleGroupItem,
+} from "@patternfly/react-core";
+import {EyeIcon, EyeSlashIcon} from "@patternfly/react-icons";
+import {hasDigit, hasLowercase, hasMinimumLength, hasSpecialCharacter, 
hasUppercase} from "@utils/StringUtils";
+import {SimpleSelect, TypeaheadSelect, TypeaheadSelectOption} from 
"@patternfly/react-templates";
+import {SimpleSelectOption} from 
"@patternfly/react-templates/src/components/Select/SimpleSelect";
+import {MonacoEditor} from "@shared/MonacoEditor";
+import {useAppConfig} from "@compass/useConfig";
+
+export function useFormUtil(formContext: UseFormReturn<any>) {
+
+    const [showPassword, setShowPassword] = useState<boolean>(false);
+    const {isDev} = useAppConfig();
+
+    function getError(error: FieldError | undefined) {
+        if (error) {
+            return (
+                <FormHelperText>
+                    <HelperText>
+                        <HelperTextItem variant={'error'}>
+                            {error.message}
+                        </HelperTextItem>
+                    </HelperText>
+                </FormHelperText>
+            )
+        } else return (<></>)
+    }
+
+    function getAlert(error: FieldError | undefined, variant?: 'success' | 
'danger' | 'warning' | 'info' | 'custom') {
+        if (error) {
+            return <Alert variant={variant} isInline isPlain 
title={error.message}/>
+        } else return (<></>)
+    }
+
+    function getHelper(text?: string) {
+        if (text) {
+            return (
+                <FormHelperText>
+                    <HelperText>
+                        <HelperTextItem variant={'default'}>
+                            {text}
+                        </HelperTextItem>
+                    </HelperText>
+                </FormHelperText>
+            )
+        } else return (<></>)
+    }
+
+    function getTextField(fieldName: string, label: string,
+                          validate?: ((value: string, formValues: any) => 
boolean | string) | Record<string, (value: string, formValues: any) => boolean 
| string>,
+                          type: | 'text' | 'date' | 'datetime-local' | 'email' 
| 'month' | 'number' | 'password' | 'search' | 'tel' | 'time' | 'url' = 'text',
+                          onChange?: ((value: any) => void), hint?: string, 
onBlur?: () => void, readonly?: boolean, placeholder?: string)  {
+        const {control, setValue, getValues, formState: {errors}} = 
formContext;
+        const rules: any = {};
+        if (validate !== undefined) {
+            rules.required = "Required field";
+        }
+        if (validate) {
+            rules.validate = validate;
+        }
+        return (
+            <FormGroup label={label} fieldId={fieldName} isRequired={validate 
!== undefined}>
+                <Controller
+                    rules={rules}
+                    control={control}
+                    name={fieldName}
+                    render={({ field }) => (
+                        <TextInput className="text-field"
+                                   type={type}
+                                   id={fieldName}
+                                   ref={field?.ref}
+                                   required={validate !== undefined}
+                                   readOnly={readonly}
+                                   value={getValues(fieldName) || ''}
+                                   placeholder={placeholder}
+                                   validated={errors[fieldName] ? 'error' : 
'default'}
+                                   onChange={(_, rawValue) => {
+                                       field.onChange(rawValue);
+                                       onChange?.(rawValue);
+                                   }}
+                                   onBlur={event => onBlur?.()}
+                        />
+                    )}
+                />
+                {getError((errors as any)[fieldName])}
+                {getHelper(hint)}
+            </FormGroup>
+        )
+    }
+
+    function getTextFieldForId(fieldName: string, label: string, validate?: 
((value: string, formValues: any) => boolean | string) | Record<string, (value: 
string, formValues: any) => boolean | string>) {
+        const {control, getValues, formState: {errors}} = formContext;
+        const rules: any = {};
+        rules.required = "Required field";
+        if (validate) {
+            rules.validate = validate;
+        }
+        return (
+            <FormGroup label={label} fieldId={fieldName} isRequired={validate 
!== undefined}>
+                <Controller
+                    rules={rules}
+                    control={control}
+                    name={fieldName}
+                    render={({ field }) => (
+                        <TextInput className="text-field"
+                                   type={"text"}
+                                   id={fieldName}
+                                   ref={field?.ref}
+                                   required={validate !== undefined}
+                                   value={getValues(fieldName) || ''}
+                                   validated={errors[fieldName] ? 'error' : 
'default'}
+                                   onChange={(_, rawValue) => {
+                                       const sanitizedValue = 
rawValue.replace(/[^a-zA-Z0-9]/g, '');
+                                       field.onChange(sanitizedValue);
+                                   }}
+                        />
+                    )}
+                />
+                {getError((errors as any)[fieldName])}
+            </FormGroup>
+        )
+    }
+
+    function getTextFieldForApp(fieldName: string, label: string, validate?: 
((value: string, formValues: any) => boolean | string) | Record<string, (value: 
string, formValues: any) => boolean | string>) {
+        const {control, getValues, formState: {errors}} = formContext;
+        const rules: any = {};
+        rules.required = "Required field";
+        if (validate) {
+            rules.validate = validate;
+        }
+        return (
+            <FormGroup label={label} fieldId={fieldName} isRequired={validate 
!== undefined}>
+                <Controller
+                    rules={rules}
+                    control={control}
+                    name={fieldName}
+                    render={({ field }) => (
+                        <TextInput className="text-field"
+                                   type={"text"}
+                                   id={fieldName}
+                                   ref={field?.ref}
+                                   required={validate !== undefined}
+                                   value={getValues(fieldName) || ''}
+                                   validated={errors[fieldName] ? 'error' : 
'default'}
+                                   onChange={(_, rawValue) => {
+                                       const sanitizedValue = rawValue
+                                           .toLowerCase() // Force lower case
+                                           .replace(/[^a-z0-9-]/g, '') // 
Remove everything except lowercase, numbers, and dashes
+                                           .replace(/^[-0-9]+/, ''); // Remove 
any dashes or numbers that appear at the very start
+                                       field.onChange(sanitizedValue);
+                                   }}
+                        />
+                    )}
+                />
+                {getError((errors as any)[fieldName])}
+            </FormGroup>
+        )
+    }
+
+    function getTextArea(fieldName: string, label: string, rows: number = 1, 
validate?: ((value: string, formValues: any) => boolean | string) | 
Record<string, (value: string, formValues: any) => boolean | string>) {
+        const {setValue, getValues, control, formState: {errors}} = 
formContext;
+        const rules: any = {};
+        if (validate !== undefined) {
+            rules.required = "Required field";
+        }
+        if (validate) {
+            rules.validate = validate;
+        }
+        return (
+            <FormGroup label={label} fieldId={fieldName} isRequired={validate 
!== undefined}>
+                <Controller
+                    rules={rules}
+                    control={control}
+                    name={fieldName}
+                    render={() => (
+                        <TextArea type="text"
+                                  id={fieldName}
+                                  rows={rows}
+                                  value={getValues(fieldName) || ''}
+                                  validated={errors[fieldName] ? 'error' : 
'default'}
+                            // ref={ref}
+                                  onChange={(e, v) => {
+                                      setValue(fieldName, v, {shouldValidate: 
true});
+                                  }}
+                                  autoResize
+                        />
+                    )}
+                />
+                {getError((errors as any)[fieldName])}
+            </FormGroup>
+        )
+    }
+
+    function getPasswordField(fieldName: string, label: string, validate?: 
((value: string, formValues: any) => boolean | string) | Record<string, (value: 
string, formValues: any) => boolean | string>) {
+        validate = {
+            length: v => hasMinimumLength(v) || 'Password should be at least 8 
characters',
+            lower: v => hasLowercase(v) || 'Password should have at least one 
lowercase letter',
+            upper: v => hasUppercase(v) || 'Password should have at least one 
uppercase letter',
+            digit: v => hasDigit(v) || 'Password should have at least one 
digit',
+            special: v => hasSpecialCharacter(v) || 'Password should have at 
least one special character',
+        }
+        const {control, setValue, getValues, formState: {errors}} = 
formContext;
+        return (
+            <FormGroup label={label} fieldId={fieldName} isRequired>
+                <Controller
+                    rules={{required: "Required field", validate: validate}}
+                    control={control}
+                    name={fieldName}
+                    render={() => (
+                        <div style={{display: 'flex'}}>
+                            <TextInput className="text-field" 
type={showPassword ? "text" : "password"} id={fieldName}
+                                       value={getValues(fieldName) || ''}
+                                       validated={errors[fieldName] ? 'error' 
: 'default'}
+                                       onChange={(_, v) => {
+                                           setValue(fieldName, v, 
{shouldValidate: true});
+                                       }}
+                            />
+                            <Button variant="control" onClick={e => 
setShowPassword(!showPassword)}>
+                                {showPassword ? <EyeIcon/> : <EyeSlashIcon/>}
+                            </Button>
+                        </div>
+                    )}
+                />
+                {getHelper((errors as any)[fieldName])}
+            </FormGroup>
+        )
+    }
+
+    function getTextFieldPrefix(fieldName: string, label: string, prefix: 
string,
+                                required: boolean,
+                                validate?: ((value: string, formValues: any) 
=> boolean | string) | Record<string, (value: string, formValues: any) => 
boolean | string>) {
+        const {setValue, getValues, register, formState: {errors}} = 
formContext;
+        return (
+            <FormGroup label={label} fieldId={fieldName} isRequired>
+                <TextInputGroup>
+                    <TextInputGroupMain className="text-field-with-prefix" 
type="text" id={fieldName}
+                                        value={getValues(fieldName)}
+                                        {...register(fieldName, {
+                                            required: (required ? "Required 
field" : false),
+                                            validate: validate
+                                        })}
+                                        onChange={(e, v) => {
+                                            setValue(fieldName, v, 
{shouldValidate: true});
+                                        }}
+                    >
+                        <Content className='text-field-prefix' 
component='p'>{prefix}</Content>
+                    </TextInputGroupMain>
+                </TextInputGroup>
+                {getHelper((errors as any)[fieldName])}
+            </FormGroup>
+        )
+    }
+
+    function getTextFieldSuffix(fieldName: string, label: string, suffix: 
string,
+                                validate?: ((value: string, formValues: any) 
=> boolean | string) | Record<string, (value: string, formValues: any) => 
boolean | string>,
+                                type: | 'text' | 'date' | 'datetime-local' | 
'email' | 'month' | 'number' | 'password' | 'search' | 'tel' | 'time' | 'url' = 
'text') {
+        const {control, setValue, getValues, formState: {errors}} = 
formContext;
+        return (
+            <FormGroup label={label} fieldId={fieldName} isRequired>
+                <Controller
+                    rules={{required: "Required field", validate: validate}}
+                    control={control}
+                    name={fieldName}
+                    render={() => (
+                        <div style={{display: 'flex'}}>
+                            <TextInput className="form-util-text-field" 
type={type} id={fieldName}
+                                       value={getValues(fieldName)}
+                                       validated={errors[fieldName] ? 'error' 
: 'default'}
+                                       onChange={(_, v) => {
+                                           setValue(fieldName, v, 
{shouldValidate: true});
+                                       }}
+                            />
+                            <TextInput className="form-util-text-field-suffix" 
id={fieldName + ':suffix'} value={suffix} isDisabled/>
+                        </div>
+                    )}
+                />
+                {getHelper((errors as any)[fieldName])}
+            </FormGroup>
+        )
+    }
+
+    function getFormSelect(fieldName: string, label: string, options: [string, 
string][]) {
+        const {register, watch, setValue, formState: {errors}} = formContext;
+        return (
+            <FormGroup label={label} fieldId={fieldName} isRequired>
+                <FormSelect
+                    id={fieldName}
+                    ouiaId={fieldName}
+                    validated={errors[fieldName] ? 'error' : 'default'}
+                    value={watch(fieldName)}
+                    {...register(fieldName, {required: "Required field"})}
+                    onChange={(e, v) => {
+                        setValue(fieldName, v, {shouldValidate: true});
+                    }}
+                    name={fieldName}
+
+                >
+                    <FormSelectOption key='placeholder' value={undefined} 
label='Select one' isDisabled/>
+                    {options.map((option, index) => (
+                        <FormSelectOption key={index} value={option[0]} 
label={option[1]}/>
+                    ))}
+                </FormSelect>
+                {getHelper((errors as any)[fieldName])}
+            </FormGroup>
+        )
+    }
+
+    function getTypeaheadSelect(fieldName: string, label: string, options: 
TypeaheadSelectOption[], readonly?: boolean, isCreatable?: boolean) {
+        // 1. Destructure what you need. Note: register is needed to 
"register" the field logic,
+        // but NOT to spread props onto the FormGroup.
+        const { register, watch, setValue, formState: { errors } } = 
formContext;
+
+        // 2. Register the field silently so RHF knows it exists (for 
validation/submit),
+        // but don't attach the props to the UI component.
+        React.useEffect(() => {
+            register(fieldName);
+        }, [register, fieldName]);
+
+        const value = watch(fieldName);
+        const initialOptions: TypeaheadSelectOption[] = options.map(o => ({
+            ...o,
+            value: o.value ?? '', // Ensure value is robust
+            selected: o.value === value
+        }));
+
+        const isNewValue = (value !== null && value !== undefined && 
!initialOptions.find((option) => option.value === value));
+
+        return (
+            // [FIX]: Removed {...register(fieldName)} from here
+            <FormGroup label={label} fieldId={fieldName}>
+                <TypeaheadSelect
+                    isDisabled={readonly}
+                    // Key forces re-render if switching between known/new 
values (optional optimization)
+                    key={fieldName}
+                    id={fieldName}
+                    ouiaId={fieldName}
+                    initialOptions={initialOptions}
+                    createOptionMessage={`New ${label} "${value}"`}
+                    isCreatable={isCreatable}
+                    isCreateOptionOnTop={true}
+                    placeholder={`Select ${label}`}
+
+                    // Handle text input changes (typing)
+                    onInputChange={(newVal: string) => {
+                        if (isCreatable) {
+                            setValue(fieldName, newVal, { shouldValidate: 
false, shouldDirty: true });
+                        }
+                    }}
+
+                    // Handle selection from the list
+                    onSelect={(_ev, selection) => {
+                        setValue(fieldName, selection, { shouldValidate: true, 
shouldDirty: true });
+                    }}
+
+                    // Handle pressing Enter to create a new tag
+                    onKeyDown={(event: React.KeyboardEvent<HTMLInputElement>) 
=> {
+                        if (isCreatable && event.key === 'Enter') {
+                            event.preventDefault(); // Stop form submission
+                            event.stopPropagation();
+                            const inputVal = (event.target as 
HTMLInputElement).value;
+                            setValue(fieldName, inputVal, { shouldValidate: 
true, shouldDirty: true });
+                        }
+                    }}
+                />
+                {/* Helper/Error display */}
+                {getHelper((errors as any)[fieldName])}
+            </FormGroup>
+        );
+    }
+
+    function getTypeaheadSelectNotCreatable(fieldName: string, label: string, 
options: TypeaheadSelectOption[]) {
+        const { register, watch, setValue, formState: { errors } } = 
formContext;
+        React.useEffect(() => {
+            register(fieldName);
+        }, [register, fieldName]);
+
+        const value = watch(fieldName);
+        const initialOptions: TypeaheadSelectOption[] = options.map(o => ({
+            ...o,
+            value: o.value ?? '', // Ensure value is robust
+            selected: o.value === value
+        }));
+
+        return (
+            <FormGroup label={label} fieldId={fieldName}>
+                <TypeaheadSelect
+                    key={`${fieldName}-${value ?? 'empty'}`}
+                    id={fieldName}
+                    ouiaId={fieldName}
+                    initialOptions={initialOptions}
+                    placeholder={`Select ${label}`}
+                    onSelect={(_ev, selection) => {
+                        setValue(fieldName, selection, { shouldValidate: true, 
shouldDirty: true });
+                    }}
+                />
+                {getHelper((errors as any)[fieldName])}
+            </FormGroup>
+        );
+    }
+
+    const ControlledSimpleSelect = ({
+                                        fieldName,
+                                        label,
+                                        options,
+                                        formContext
+                                    }: {
+        fieldName: string;
+        label: string;
+        options: SimpleSelectOption[];
+        formContext: UseFormReturn<any>;
+    }) => {
+        const { register, watch, setValue, formState: { errors } } = 
formContext;
+
+        React.useEffect(() => {
+            register(fieldName);
+        }, [register, fieldName]);
+
+        const value = watch(fieldName);
+        const initialOptions: SimpleSelectOption[] = options.map(o => ({
+            ...o,
+            value: o.value ?? '',
+            selected: o.value === value
+        }));
+
+        return (
+            <FormGroup label={label} fieldId={fieldName}>
+                <SimpleSelect
+                    toggleWidth={'100%'}
+                    key={`${fieldName}-${value ?? 'empty'}`}
+                    id={fieldName}
+                    ouiaId={fieldName}
+                    initialOptions={initialOptions}
+                    placeholder={`Select ${label}`}
+                    onSelect={(_ev, selection) => {
+                        setValue(fieldName, selection, { shouldValidate: true, 
shouldDirty: true });
+                    }}
+                    isScrollable={true}
+                    popperProps={{ position: 'end' }}
+                />
+                {/* Make sure getHelper is available here, or pass the error 
string directly */}
+                {getHelper((errors as any)[fieldName])}
+            </FormGroup>
+        );
+    };
+
+    function getSimpleSelect(fieldName: string, label: string, options: 
SimpleSelectOption[]) {
+        return (
+            <ControlledSimpleSelect
+                key={fieldName} // Important: gives React a stable identity
+                fieldName={fieldName}
+                label={label}
+                options={options}
+                formContext={formContext}
+            />
+        );
+    }
+
+
+    function getSwitches(fieldName: string, label: string, options: [string, 
string][]) {
+        const {watch, register, getValues, setValue, formState: {errors}} = 
formContext;
+        return (
+            <FormGroup label={label} fieldId={fieldName} isRequired 
{...register(fieldName)}>
+                <Flex direction={{default: 'column'}}>
+                    {options.map((option, index) => {
+                        const key = option[0];
+                        const label = option[0];
+                        return (<Switch
+                            id={key}
+                            label={label}
+                            isChecked={watch(fieldName) !== undefined && 
watch(fieldName).includes(key)}
+                            onChange={(e, v) => {
+                                const vals: string[] = watch(fieldName);
+                                const idx = vals.findIndex(x => x === key);
+                                if (idx > -1 && !v) {
+                                    vals.splice(idx, 1);
+                                    setValue(fieldName, [...vals]);
+                                } else if (idx === -1 && v) {
+                                    vals.push(key);
+                                    setValue(fieldName, [...vals]);
+                                }
+                            }}
+                            ouiaId={option[0]}
+                        />)
+                    })}
+                </Flex>
+            </FormGroup>
+        )
+    }
+
+    function getCheckbox(fieldName: string, label: string) {
+        const {watch, control, setValue, formState: {errors}} = formContext;
+        const value = watch(fieldName)
+        return (
+            <FormGroup label={label} fieldId={fieldName} key={fieldName}>
+                <Controller
+                    control={control}
+                    name={fieldName}
+                    render={({ field }) => {
+                        return (
+                            <Checkbox id='exchangePattern'
+                                      isChecked={value}
+                                      onChange={(_, checked) => 
setValue(fieldName, checked, {shouldValidate: false})}
+                            />
+                        )
+                    }}/>
+            </FormGroup>
+        )
+    }
+
+    function getToggleGroup(fieldName: string, label: string, options: 
string[], onChange?: (option: string, isSelected: boolean) => void) {
+        const {control, formState: {errors}} = formContext;
+        return (
+            <FormGroup label={label} fieldId={fieldName}>
+                <Controller
+                    control={control}
+                    name={fieldName}
+                    render={({field}) => (
+                        <ToggleGroup aria-label="ToggleGroup" 
className='combinations-toggle-group'>
+                            {options.map((option) => {
+                                return (
+                                    <ToggleGroupItem
+                                        key={option}
+                                        text={capitalize(option)}
+                                        isSelected={option === field.value}
+                                        onChange={(_, isSelected) => {
+                                            field.onChange(isSelected ? option 
: undefined);
+                                            onChange?.(option, isSelected)
+                                        }}
+                                    />
+                                )
+                            })}
+                        </ToggleGroup>
+                    )}
+                />
+                {getError((errors as any)[fieldName])}
+            </FormGroup>
+        )
+    }
+
+    function getMonacoEditor(
+        fieldName: string,
+        language: string = 'markdown',
+        height: string = '370px',
+        onChange?: ((value: string | undefined) => void),
+        validate?: ((value: string, formValues: any) => boolean | string) | 
Record<string, (value: string, formValues: any) => boolean | string>,
+        onBlur?: () => void
+    ) {
+        const { control, setValue, getValues, formState: { errors } } = 
formContext;
+        const rules: any = {};
+
+        if (validate !== undefined) {
+            rules.required = "Required field";
+        }
+        if (validate) {
+            rules.validate = validate;
+        }
+
+        const hasError = !!errors[fieldName];
+
+        return (
+            <Controller
+                rules={rules}
+                control={control}
+                name={fieldName}
+                render={() => (
+                    <div
+                        className={`monaco-wrapper ${hasError ? 'has-error' : 
''}`}
+                        style={{
+                            border: hasError
+                                ? '1px solid 
var(--pf-v5-global--danger-color--100, #c9190b)'
+                                : '1px solid 
var(--pf-v5-global--BorderColor--100, #d2d2d2)',
+                            borderRadius: '3px',
+                            overflow: 'hidden',
+                            display: 'flex',
+                            flexDirection: 'column',
+                            flexGrow: 1,
+                            minHeight: 0
+                        }}
+                    >
+                        <MonacoEditor
+                            height={height} // Tells Monaco to fill the div
+                            language={language}
+                            value={getValues(fieldName) || ''}
+                            onChange={(v) => {
+                                setValue(fieldName, v, { shouldValidate: true 
});
+                                onChange?.(v);
+                            }}
+                            onMount={(editor) => {
+                                editor.onDidBlurEditorText(() => {
+                                    onBlur?.();
+                                });
+                            }}
+                            options={{
+                                minimap: { enabled: false },
+                                scrollBeyondLastLine: false,
+                                lineNumbersMinChars: 2,
+                                automaticLayout: true, // Crucial for 
responsive resizing in a Drawer
+                                readOnly: !isDev
+                            }}
+                        />
+                    </div>
+                )}
+            />
+        );
+    }
+
+    return {
+        getFormSelect, getTextField, getSwitches, getTextFieldPrefix, 
getTextArea, getPasswordField, getTextFieldSuffix, getCheckbox, getSimpleSelect
+    }
+}
diff --git a/karavan-app/src/main/webui/src/ui/shared/icons/logo.svg 
b/karavan-app/src/main/webui/src/ui/shared/icons/logo.svg
new file mode 100644
index 00000000..61f89cfe
--- /dev/null
+++ b/karavan-app/src/main/webui/src/ui/shared/icons/logo.svg
@@ -0,0 +1,178 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+        width="360.13885"
+        height="360.13885"
+        viewBox="0 0 360.13885 360.13885"
+        version="1.1"
+        preserveAspectRatio="xMidYMid"
+        id="svg50"
+        xmlns:xlink="http://www.w3.org/1999/xlink";
+        xmlns="http://www.w3.org/2000/svg";>
+    <defs
+            id="defs31">
+        <linearGradient
+                id="linearGradient1351">
+            <stop
+                    style="stop-color:#dcffff;stop-opacity:1"
+                    offset="0"
+                    id="stop1347" />
+            <stop
+                    style="stop-color:#96d2e6;stop-opacity:1"
+                    offset="1"
+                    id="stop1349" />
+        </linearGradient>
+        <circle
+                id="path-1"
+                cx="128"
+                cy="128.00015"
+                r="128" />
+        <linearGradient
+                x1="-26.051073"
+                y1="271.33054"
+                x2="254.31573"
+                y2="0.047514945"
+                id="linearGradient-3"
+                gradientUnits="userSpaceOnUse">
+            <stop
+                    stop-color="#F69923"
+                    offset="0%"
+                    id="stop10"
+                    style="stop-color:#4790bb;stop-opacity:1" />
+            <stop
+                    stop-color="#F79A23"
+                    offset="10.996%"
+                    id="stop12"
+                    style="stop-color:#64b7db;stop-opacity:1" />
+            <stop
+                    stop-color="#E97826"
+                    offset="94.502%"
+                    id="stop14"
+                    style="stop-color:#326ea0;stop-opacity:1" />
+        </linearGradient>
+        <linearGradient
+                x1="-32.163429"
+                y1="277.02905"
+                x2="259.33835"
+                y2="-5.0281582"
+                id="linearGradient-4"
+                gradientUnits="userSpaceOnUse">
+            <stop
+                    stop-color="#F69923"
+                    offset="0%"
+                    id="stop17" />
+            <stop
+                    stop-color="#F79A23"
+                    offset="8.0478%"
+                    id="stop19" />
+            <stop
+                    stop-color="#E97826"
+                    offset="41.874%"
+                    id="stop21" />
+        </linearGradient>
+        <linearGradient
+                x1="217.94496"
+                y1="67.504837"
+                x2="99.458817"
+                y2="247.00549"
+                id="linearGradient-5"
+                gradientTransform="scale(0.96441978,1.0368929)"
+                gradientUnits="userSpaceOnUse"
+                xlink:href="#linearGradient-4">
+            <stop
+                    stop-color="#F6E423"
+                    offset="0%"
+                    id="stop24"
+                    style="stop-color:#92d6d5;stop-opacity:1" />
+            <stop
+                    stop-color="#F79A23"
+                    offset="41.191%"
+                    id="stop26"
+                    style="stop-color:#79b7cc;stop-opacity:1" />
+            <stop
+                    stop-color="#E97826"
+                    offset="73.271%"
+                    id="stop28"
+                    style="stop-color:#5891c5;stop-opacity:1" />
+        </linearGradient>
+        <mask
+                id="mask-2"
+                fill="#ffffff">
+            <use
+                    xlink:href="#path-1"
+                    id="use33"
+                    x="0"
+                    y="0"
+                    width="100%"
+                    height="100%" />
+        </mask>
+        <mask
+                id="mask-2-7"
+                fill="#ffffff">
+            <use
+                    xlink:href="#path-1"
+                    id="use137"
+                    x="0"
+                    y="0"
+                    width="100%"
+                    height="100%" />
+        </mask>
+        <linearGradient
+                xlink:href="#linearGradient1351"
+                id="linearGradient1345"
+                x1="233.12198"
+                y1="56.01545"
+                x2="2.2396212"
+                y2="242.78015"
+                gradientUnits="userSpaceOnUse" />
+    </defs>
+    <g
+            id="g36"
+            transform="translate(45.104634,56.041665)" />
+    <circle
+            fill="url(#linearGradient-3)"
+            fill-rule="nonzero"
+            mask="url(#mask-2)"
+            cx="127.99429"
+            cy="127.99429"
+            r="123.11053"
+            id="circle38"
+            style="fill:url(#linearGradient-3)"
+            transform="translate(45.104634,56.041665)" />
+    <g
+            id="g2266"
+            transform="translate(45.104634,56.041665)">
+        <path
+                d="m 98.043695,75.516752 c -1.750682,-0.002 -3.524167,0.0098 
-5.292059,0.06144 -2.05519,0.06065 -4.816316,0.713182 -7.999625,1.784532 
53.775199,40.834016 73.108199,114.497516 39.875049,178.514206 1.12865,0.0293 
2.24876,0.12307 3.38456,0.12307 60.7361,0 111.49261,-42.32269 
124.60904,-99.07129 C 214.07872,111.75077 161.80794,75.61511 
98.043301,75.516654 Z"
+                fill="url(#linearGradient-5)"
+                fill-rule="nonzero"
+                opacity="0.75"
+                mask="url(#mask-2)"
+                id="path42"
+                style="fill:url(#linearGradient-5);fill-opacity:1" />
+    </g>
+    <path
+            d="M 84.752011,77.367742 C 66.89547,83.377158 32.82977,104.54579 
0.07906091,132.8108 2.5662796,200.14549 57.107277,254.12351 124.62706,255.88195 
157.86021,191.86526 138.528,118.20176 84.752011,77.367742 Z"
+            fill="#28170b"
+            fill-rule="nonzero"
+            opacity="0.75"
+            mask="url(#mask-2)"
+            id="path44"
+            style="fill:#1e4b7b;fill-opacity:1"
+            transform="translate(45.104634,56.041665)" />
+    <path
+            d="m 128.74719,54.004528 c -10.98485,5.495372 0,27.466068 
0,27.466068 -32.973011,27.483724 -25.9672,74.429124 -64.435392,74.429124 
-20.970343,0 -42.242226,-24.0768 -64.23273709,-38.82804 -0.28309669,3.47897 
-0.78535974,6.97247 -0.78535974,10.52442 0,48.09504 26.26287383,89.92436 
65.41967783,111.89721 10.952683,-1.3796 22.838636,-4.11444 31.050991,-9.59255 
43.14527,-28.76482 53.85703,-83.49096 71.48633,-109.92509 10.97897,-16.492 
62.43429,-15.06102 65.90679,-22.01013 5.50126,- [...]
+            fill="#ffffff"
+            fill-rule="nonzero"
+            mask="url(#mask-2-7)"
+            id="path150"
+            transform="translate(44.335601,55.908568)"
+            style="fill:url(#linearGradient1345);fill-opacity:1" />
+    <path
+            d="M 128,256 C 57.307552,256 0,198.69245 0,128 0,57.307552 
57.307552,0 128,0 c 70.69245,0 128,57.307552 128,128 0,70.69245 -57.30755,128 
-128,128 z m 0,-9.76795 C 193.29776,246.23205 246.23205,193.29776 246.23205,128 
246.23205,62.702243 193.29776,9.7679529 128,9.7679529 62.702243,9.7679529 
9.7679529,62.702243 9.7679529,128 9.7679529,193.29776 62.702243,246.23205 
128,246.23205 Z"
+            fill="url(#linearGradient-4)"
+            fill-rule="nonzero"
+            mask="url(#mask-2)"
+            id="path40"
+            style="fill:#2d4150;fill-opacity:1"
+            transform="matrix(1.020345,0,0,1.020345,41.979634,53.958328)" />
+</svg>
\ No newline at end of file

Reply via email to