diff --git a/src/api/workflowApi.ts b/src/api/workflowApi.ts
index 7254640..ce902c1 100644
--- a/src/api/workflowApi.ts
+++ b/src/api/workflowApi.ts
@@ -57,8 +57,8 @@ export interface StartWorkflowResponse extends Workflow {
export interface ChatDataResponse {
messages: WorkflowMessage[];
logs: WorkflowLog[];
- stats: WorkflowStats[];
documents: WorkflowDocument[];
+ workflowCost: number;
}
// Type for the request function passed to API functions
@@ -259,35 +259,25 @@ export async function fetchChatData(
console.log('📥 fetchChatData response:', data);
- // Handle unified items format: { items: [{ type: 'message'|'log'|'stat', item: {...}, createdAt: ... }] }
+ const workflowCost: number = data.workflowCost ?? 0;
+
if (data.items && Array.isArray(data.items)) {
const messages: WorkflowMessage[] = [];
const logs: WorkflowLog[] = [];
- const stats: WorkflowStats[] = [];
const documents: WorkflowDocument[] = [];
data.items.forEach((item: any) => {
if (item.type === 'message') {
- // Handle both formats: item.item or direct item data
const messageData = item.item || item;
if (messageData && (messageData.id || messageData.message)) {
messages.push(messageData);
- } else {
- console.warn('⚠️ Invalid message item:', item);
}
} else if (item.type === 'log') {
const logData = item.item || item;
if (logData) {
logs.push(logData);
}
- } else if (item.type === 'stat') {
- const statData = item.item || item;
- if (statData) {
- stats.push(statData);
- }
- }
- // Documents might be in items or separate
- if (item.type === 'document') {
+ } else if (item.type === 'document') {
const docData = item.item || item;
if (docData) {
documents.push(docData);
@@ -295,27 +285,19 @@ export async function fetchChatData(
}
});
- console.log('📦 Extracted from items:', {
- messages: messages.length,
- logs: logs.length,
- stats: stats.length,
- documents: documents.length
- });
-
return {
messages,
logs,
- stats,
- documents: documents.length > 0 ? documents : (Array.isArray(data.documents) ? data.documents : [])
+ documents: documents.length > 0 ? documents : (Array.isArray(data.documents) ? data.documents : []),
+ workflowCost
};
}
- // Fallback to direct format: { messages: [], logs: [], stats: [] }
return {
messages: Array.isArray(data.messages) ? data.messages : [],
logs: Array.isArray(data.logs) ? data.logs : [],
- stats: Array.isArray(data.stats) ? data.stats : [],
- documents: Array.isArray(data.documents) ? data.documents : []
+ documents: Array.isArray(data.documents) ? data.documents : [],
+ workflowCost
};
}
diff --git a/src/components/Navigation/UserSection.tsx b/src/components/Navigation/UserSection.tsx
index e19cd0e..1b6f9e7 100644
--- a/src/components/Navigation/UserSection.tsx
+++ b/src/components/Navigation/UserSection.tsx
@@ -49,9 +49,12 @@ export const UserSection: React.FC = () => {
}
// Initialen fĂĽr Avatar
- const initials = user.fullName
- ? user.fullName.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
- : user.username.slice(0, 2).toUpperCase();
+ const initials = (() => {
+ const name = user.fullName || user.username || '';
+ const parts = name.trim().split(/\s+/);
+ if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toLocaleUpperCase();
+ return [...name.trim()].slice(0, 2).join('').toLocaleUpperCase() || '?';
+ })();
return (
diff --git a/src/components/UiComponents/WorkflowStatus/WorkflowStatus.tsx b/src/components/UiComponents/WorkflowStatus/WorkflowStatus.tsx
index 59031c8..41d9692 100644
--- a/src/components/UiComponents/WorkflowStatus/WorkflowStatus.tsx
+++ b/src/components/UiComponents/WorkflowStatus/WorkflowStatus.tsx
@@ -2,114 +2,37 @@ import React, { useMemo } from 'react';
import { WorkflowStatusProps, WorkflowStatusType } from './WorkflowStatusTypes';
import styles from './WorkflowStatus.module.css';
-// Helper function to extract workflow status and round from log message
+const _STATUS_MAP: Record = {
+ success: 'completed',
+ completed: 'completed',
+ started: 'started',
+ running: 'started',
+ resumed: 'resumed',
+ stopped: 'stopped',
+ failed: 'failed',
+ error: 'failed',
+};
+
const extractWorkflowStatus = (logs: any[]): { status: WorkflowStatusType; round: number | null; timestamp: number } => {
- // First, check for completion messages with success status (these take priority)
- const completionMessages = logs.filter(log => {
- const message = (log.message || '').toLowerCase();
+ if (!logs.length) return { status: null, round: null, timestamp: 0 };
+
+ const sorted = [...logs].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
+
+ for (const log of sorted) {
const logStatus = (log.status || '').toLowerCase();
- return (message.includes('fast path completed') ||
- message.includes('completed successfully')) &&
- logStatus === 'success';
- });
-
- // If we have completion messages, use the latest one
- if (completionMessages.length > 0) {
- const latestCompletion = completionMessages.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))[0];
-
- // Try to extract round from completion message
- let round: number | null = null;
- const message = (latestCompletion.message || '').toLowerCase();
- const roundMatch = message.match(/\(?round\s+(\d+)\)?/i);
- if (roundMatch) {
- round = parseInt(roundMatch[1], 10);
- } else {
- // If no round in completion message, get round from latest workflow status message
- const statusMessages = logs.filter(log => {
- const msg = (log.message || '').toLowerCase();
- return msg.includes('workflow started') || msg.includes('workflow resumed');
- });
- if (statusMessages.length > 0) {
- const latestWorkflowStatus = statusMessages.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))[0];
- const workflowMessage = (latestWorkflowStatus.message || '').toLowerCase();
- const workflowRoundMatch = workflowMessage.match(/\(?round\s+(\d+)\)?/i);
- if (workflowRoundMatch) {
- round = parseInt(workflowRoundMatch[1], 10);
- }
- }
+ const mapped = _STATUS_MAP[logStatus];
+ if (mapped) {
+ const roundMatch = (log.message || '').match(/\(?round\s+(\d+)\)?/i);
+ return { status: mapped, round: roundMatch ? parseInt(roundMatch[1], 10) : null, timestamp: log.timestamp || 0 };
}
-
- return {
- status: 'completed',
- round,
- timestamp: latestCompletion.timestamp || 0
- };
}
- // If no completion messages, look for workflow started/resumed/stopped messages
- const statusMessages = logs.filter(log => {
- const message = (log.message || '').toLowerCase();
- return message.includes('workflow started') ||
- message.includes('workflow resumed') ||
- message.includes('workflow stopped') ||
- message.includes('workflow failed') ||
- message.includes('workflow completed');
- });
-
- if (statusMessages.length === 0) {
- return { status: null, round: null, timestamp: 0 };
- }
-
- // Get the latest status message
- const latestStatus = statusMessages.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))[0];
- const message = (latestStatus.message || '').toLowerCase();
-
- let status: WorkflowStatusType = null;
- if (message.includes('started')) {
- status = 'started';
- } else if (message.includes('resumed')) {
- status = 'resumed';
- } else if (message.includes('stopped')) {
- status = 'stopped';
- } else if (message.includes('failed')) {
- status = 'failed';
- } else if (message.includes('completed')) {
- status = 'completed';
- }
-
- // Extract round number from message (e.g., "round 4", "round 2", or "(round 4)")
- const roundMatch = message.match(/\(?round\s+(\d+)\)?/i);
- const round = roundMatch ? parseInt(roundMatch[1], 10) : null;
-
- return {
- status,
- round,
- timestamp: latestStatus.timestamp || 0
- };
+ return { status: null, round: null, timestamp: 0 };
};
-// Helper function to format bytes to KB or MB
-const formatBytes = (bytes?: number): string => {
- if (bytes === undefined || bytes === null) return '-';
- if (bytes === 0) return '0 B';
- const kb = bytes / 1024;
- if (kb < 1024) {
- return `${kb.toFixed(2)} KB`;
- }
- const mb = kb / 1024;
- return `${mb.toFixed(2)} MB`;
-};
-
-// Helper function to format price
-const formatPrice = (price?: number): string => {
- if (price === undefined || price === null) return '-';
- return `$${price.toFixed(2)}`;
-};
-
-// Helper function to format processing time
-const formatProcessingTime = (time?: number): string => {
- if (time === undefined || time === null) return '-';
- return `${time.toFixed(2)}s`;
+const _formatCurrency = (amount?: number): string => {
+ if (amount === undefined || amount === null) return '-';
+ return `${amount.toFixed(2)} CHF`;
};
const WorkflowStatus: React.FC = ({
@@ -122,40 +45,10 @@ const WorkflowStatus: React.FC = ({
}) => {
// Use workflow status and round from API response, fallback to extracting from logs
const workflowStatus = useMemo(() => {
- // If we have status from API, use it
if (workflowStatusFromApi) {
- let status: WorkflowStatusType = null;
- const statusLower = workflowStatusFromApi.toLowerCase();
-
- if (statusLower === 'completed') {
- status = 'completed';
- } else if (statusLower === 'running') {
- // Check if it's started or resumed from logs
- const startedResumedLogs = logs.filter(log => {
- const message = (log.message || '').toLowerCase();
- return message.includes('workflow started') || message.includes('workflow resumed');
- });
- if (startedResumedLogs.length > 0) {
- const latest = startedResumedLogs.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))[0];
- const message = (latest.message || '').toLowerCase();
- status = message.includes('resumed') ? 'resumed' : 'started';
- } else {
- status = 'started';
- }
- } else if (statusLower === 'stopped') {
- status = 'stopped';
- } else if (statusLower === 'failed') {
- status = 'failed';
- }
-
- return {
- status,
- round: currentRoundFromApi || null,
- timestamp: Date.now() / 1000 // Use current time since we don't have timestamp from API
- };
+ const mapped = _STATUS_MAP[workflowStatusFromApi.toLowerCase()] || null;
+ return { status: mapped, round: currentRoundFromApi || null, timestamp: Date.now() / 1000 };
}
-
- // Fallback to extracting from logs
return extractWorkflowStatus(logs);
}, [workflowStatusFromApi, currentRoundFromApi, logs]);
@@ -185,33 +78,13 @@ const WorkflowStatus: React.FC = ({
)}
- {/* Stats Display */}
- {latestStats && (
+ {/* Cost Display */}
+ {latestStats && latestStats.priceCHF !== undefined && (
- {latestStats.priceUsd !== undefined && (
-
- Price:
- {formatPrice(latestStats.priceUsd)}
-
- )}
- {latestStats.processingTime !== undefined && (
-
- Time:
- {formatProcessingTime(latestStats.processingTime)}
-
- )}
- {latestStats.bytesSent !== undefined && (
-
- Sent:
- {formatBytes(latestStats.bytesSent)}
-
- )}
- {latestStats.bytesReceived !== undefined && (
-
- Received:
- {formatBytes(latestStats.bytesReceived)}
-
- )}
+
+ Cost:
+ {_formatCurrency(latestStats.priceCHF)}
+
)}
diff --git a/src/components/UiComponents/WorkflowStatus/WorkflowStatusTypes.ts b/src/components/UiComponents/WorkflowStatus/WorkflowStatusTypes.ts
index d83ca14..5275b9a 100644
--- a/src/components/UiComponents/WorkflowStatus/WorkflowStatusTypes.ts
+++ b/src/components/UiComponents/WorkflowStatus/WorkflowStatusTypes.ts
@@ -44,13 +44,10 @@ export interface WorkflowStatusProps {
isRunning?: boolean;
/**
- * Latest statistics from the workflow (price, processing time, bytes sent/received)
+ * Latest cost from billing transactions (single source of truth)
*/
latestStats?: {
- priceUsd?: number;
- processingTime?: number;
- bytesSent?: number;
- bytesReceived?: number;
+ priceCHF?: number;
} | null;
}
diff --git a/src/config/pageRegistry.tsx b/src/config/pageRegistry.tsx
index 902c588..f68c91c 100644
--- a/src/config/pageRegistry.tsx
+++ b/src/config/pageRegistry.tsx
@@ -116,6 +116,10 @@ export const PAGE_ICONS: Record = {
'page.feature.chatbot.conversations': ,
'feature.chatbot': ,
'feature.teamsbot': ,
+
+ // Feature pages - Workspace
+ 'page.feature.workspace.dashboard': ,
+ 'feature.workspace': ,
};
// =============================================================================
diff --git a/src/hooks/playground/useWorkflowLifecycle.ts b/src/hooks/playground/useWorkflowLifecycle.ts
index 9b1be84..085d6f7 100644
--- a/src/hooks/playground/useWorkflowLifecycle.ts
+++ b/src/hooks/playground/useWorkflowLifecycle.ts
@@ -14,8 +14,8 @@ import { useWorkflowPolling } from './useWorkflowPolling';
import { getWorkflowApiBaseUrl } from '../useWorkflows';
interface UnifiedChatDataItem {
- type: 'message' | 'log' | 'stat';
- item: WorkflowMessage | WorkflowLog | any;
+ type: 'message' | 'log';
+ item: WorkflowMessage | WorkflowLog;
createdAt: number;
}
@@ -76,13 +76,11 @@ export function useWorkflowLifecycle(instanceId: string) {
const [logs, setLogs] = useState([]);
const [dashboardLogs, setDashboardLogs] = useState([]);
const [unifiedContentLogs, setUnifiedContentLogs] = useState([]);
- const [latestStats, setLatestStats] = useState<{ priceUsd?: number; processingTime?: number; bytesSent?: number; bytesReceived?: number } | null>(null);
+ const [latestStats, setLatestStats] = useState<{ priceCHF?: number } | null>(null);
// === REFS FOR SYNC ACCESS ===
const statusRef = useRef('idle');
const lastRenderedTimestampRef = useRef(null);
- const processedStatIdsRef = useRef>(new Set());
- const cumulativeStatsRef = useRef({ priceUsd: 0, processingTime: 0, bytesSent: 0, bytesReceived: 0 });
// === KEY STATE MACHINE FLAG ===
// This flag tracks if the UI has rendered a message with status="last"
@@ -124,17 +122,15 @@ export function useWorkflowLifecycle(instanceId: string) {
}, [workflowId]);
// === CORE: Process unified chat data ===
- const processUnifiedChatData = useCallback((chatData: { messages: WorkflowMessage[]; logs: WorkflowLog[]; stats: any[] }) => {
+ const processUnifiedChatData = useCallback((chatData: { messages: WorkflowMessage[]; logs: WorkflowLog[]; workflowCost: number }) => {
console.log('🔄 Processing chat data:', {
messages: chatData.messages?.length || 0,
logs: chatData.logs?.length || 0,
- stats: chatData.stats?.length || 0
+ workflowCost: chatData.workflowCost ?? 0
});
- // Build unified timeline
const timeline: UnifiedChatDataItem[] = [];
- // Add messages
(chatData.messages || []).forEach((message: WorkflowMessage) => {
timeline.push({
type: 'message',
@@ -143,7 +139,6 @@ export function useWorkflowLifecycle(instanceId: string) {
});
});
- // Add logs
(chatData.logs || []).forEach((log: any) => {
timeline.push({
type: 'log',
@@ -152,17 +147,6 @@ export function useWorkflowLifecycle(instanceId: string) {
});
});
- // Add stats
- const rawStats = chatData.stats || [];
- rawStats.forEach((stat: any) => {
- timeline.push({
- type: 'stat',
- item: stat,
- createdAt: stat._createdAt || stat.createdAt || Date.now()
- });
- });
-
- // Sort chronologically
timeline.sort((a, b) => a.createdAt - b.createdAt);
// Update lastRenderedTimestamp
@@ -290,44 +274,9 @@ export function useWorkflowLifecycle(instanceId: string) {
return [...allLogs].sort(sortLogs);
});
- // === PROCESS STATS ===
- const statsItems = timeline.filter(item => item.type === 'stat');
-
- if (statsItems.length > 0) {
- let hasNewStats = false;
-
- statsItems.forEach(statItem => {
- const statData = statItem.item;
- const statId = statData?.id;
-
- if (statId && processedStatIdsRef.current.has(statId)) {
- return; // Skip already processed
- }
-
- if (statData) {
- hasNewStats = true;
- if (statId) {
- processedStatIdsRef.current.add(statId);
- }
-
- // Accumulate stats
- const price = statData.priceCHF ?? statData.priceUsd ?? 0;
- if (price > 0) cumulativeStatsRef.current.priceUsd += price;
- if (statData.processingTime) cumulativeStatsRef.current.processingTime += statData.processingTime;
- if (statData.bytesSent) cumulativeStatsRef.current.bytesSent += statData.bytesSent;
- if (statData.bytesReceived) cumulativeStatsRef.current.bytesReceived += statData.bytesReceived;
- }
- });
-
- if (hasNewStats) {
- setLatestStats({
- priceUsd: cumulativeStatsRef.current.priceUsd,
- processingTime: cumulativeStatsRef.current.processingTime,
- bytesSent: cumulativeStatsRef.current.bytesSent,
- bytesReceived: cumulativeStatsRef.current.bytesReceived
- });
- }
- }
+ // === UPDATE COST from billing transactions (single source of truth) ===
+ const cost = chatData.workflowCost ?? 0;
+ setLatestStats(cost > 0 ? { priceCHF: cost } : null);
}, [convertLogToFrontendFormat]);
// === POLLING FUNCTION ===
@@ -359,7 +308,7 @@ export function useWorkflowLifecycle(instanceId: string) {
console.log('📊 Polled chat data:', {
messages: chatData.messages?.length || 0,
logs: chatData.logs?.length || 0,
- stats: chatData.stats?.length || 0,
+ workflowCost: chatData.workflowCost ?? 0,
afterTimestamp
});
@@ -496,10 +445,7 @@ export function useWorkflowLifecycle(instanceId: string) {
setUnifiedContentLogs([]);
setLatestStats(null);
- // Reset refs
lastRenderedTimestampRef.current = null;
- processedStatIdsRef.current.clear();
- cumulativeStatsRef.current = { priceUsd: 0, processingTime: 0, bytesSent: 0, bytesReceived: 0 };
hasRenderedLastMessageRef.current = false;
setHasRenderedLastMessage(false);
@@ -511,13 +457,11 @@ export function useWorkflowLifecycle(instanceId: string) {
try {
console.log('📥 Loading workflow:', workflowIdToSelect);
- // Reset state
setWorkflowId(workflowIdToSelect);
lastRenderedTimestampRef.current = null;
- processedStatIdsRef.current.clear();
- cumulativeStatsRef.current = { priceUsd: 0, processingTime: 0, bytesSent: 0, bytesReceived: 0 };
hasRenderedLastMessageRef.current = false;
setHasRenderedLastMessage(false);
+ setLatestStats(null);
// Fetch workflow data
const workflowData = await fetchWorkflowApi(request, workflowIdToSelect, apiBaseUrl).catch(() => null);
@@ -544,7 +488,7 @@ export function useWorkflowLifecycle(instanceId: string) {
console.log('📥 Loaded chat data:', {
messages: chatData.messages?.length || 0,
logs: chatData.logs?.length || 0,
- stats: chatData.stats?.length || 0
+ workflowCost: chatData.workflowCost ?? 0
});
// === STATE MACHINE: Check if last message has status="last" ===
diff --git a/src/hooks/useFiles.ts b/src/hooks/useFiles.ts
index 9812b26..f680077 100644
--- a/src/hooks/useFiles.ts
+++ b/src/hooks/useFiles.ts
@@ -479,7 +479,7 @@ export function useFileOperations() {
* - Removed workflowId from FileItem creation in interfaceComponentObjects.py
* - Upload should now work correctly
*/
- const handleFileUpload = async (file: globalThis.File, workflowId?: string) => {
+ const handleFileUpload = async (file: globalThis.File, workflowId?: string, featureInstanceId?: string) => {
setUploadError(null);
setUploadingFile(true);
@@ -500,6 +500,9 @@ export function useFileOperations() {
if (workflowId) {
formData.append('workflowId', workflowId);
}
+ if (featureInstanceId) {
+ formData.append('featureInstanceId', featureInstanceId);
+ }
// FormData is now correctly configured for backend
diff --git a/src/hooks/useWorkflows.ts b/src/hooks/useWorkflows.ts
index cdc25dc..ebaf1c5 100644
--- a/src/hooks/useWorkflows.ts
+++ b/src/hooks/useWorkflows.ts
@@ -276,12 +276,7 @@ export function useUserWorkflows(options?: { instanceId?: string; featureCode?:
} else if (attr.type === 'textarea') {
fieldType = 'textarea';
} else if (attr.type === 'text') {
- // Check if it should be textarea based on name
- if (attr.name.toLowerCase().includes('description') || attr.name.toLowerCase().includes('note')) {
- fieldType = 'textarea';
- } else {
- fieldType = 'string';
- }
+ fieldType = (attr as any).multiline === true ? 'textarea' : 'string';
}
// Note: Legacy 'boolean' and 'enum' types are not in the AttributeDefinition type union
// If needed, they should be handled via type casting: (attr as any).type === 'boolean'
diff --git a/src/layouts/MainLayout.module.css b/src/layouts/MainLayout.module.css
index 3516819..668aa7c 100644
--- a/src/layouts/MainLayout.module.css
+++ b/src/layouts/MainLayout.module.css
@@ -92,6 +92,7 @@
flex: 1;
min-width: 0;
min-height: 0;
+ position: relative;
/* Let child components handle their own scrolling for sticky headers */
overflow: hidden;
background: var(--bg-primary, #ffffff);
diff --git a/src/layouts/MainLayout.tsx b/src/layouts/MainLayout.tsx
index 02ea999..7244eb0 100644
--- a/src/layouts/MainLayout.tsx
+++ b/src/layouts/MainLayout.tsx
@@ -10,8 +10,11 @@ import { Outlet, useLocation } from 'react-router-dom';
import { FeatureProvider, useFeatureStore } from '../stores/featureStore';
import { MandateNavigation } from '../components/Navigation/MandateNavigation';
import { UserSection } from '../components/Navigation/UserSection';
+import { WorkspaceKeepAlive } from '../pages/views/workspace/WorkspaceKeepAlive';
import styles from './MainLayout.module.css';
+const _WORKSPACE_ROUTE_RE = /\/mandates\/[^/]+\/workspace\/[^/]+\/dashboard/;
+
// =============================================================================
// INNER LAYOUT (mit Zugriff auf Store)
// =============================================================================
@@ -101,7 +104,12 @@ const MainLayoutInner: React.FC = () => {
className={styles.mobileLogo}
/>
-
+
+
+
+
+
+
);
diff --git a/src/pages/FeatureView.tsx b/src/pages/FeatureView.tsx
index c0a29bc..80a10e1 100644
--- a/src/pages/FeatureView.tsx
+++ b/src/pages/FeatureView.tsx
@@ -36,6 +36,10 @@ import { AutomationDefinitionsView, AutomationTemplatesView, AutomationLogsView
// CodeEditor Views
import { CodeEditorPage, CodeEditorWorkflowsPage } from './views/codeeditor';
+// Workspace Views
+import { WorkspacePage } from './views/workspace/WorkspacePage';
+import { WorkspaceSettingsPage } from './views/workspace/WorkspaceSettingsPage';
+
// Teamsbot Views
import { TeamsbotDashboardView } from './views/teamsbot/TeamsbotDashboardView';
import { TeamsbotSessionView } from './views/teamsbot/TeamsbotSessionView';
@@ -137,6 +141,10 @@ const VIEW_COMPONENTS: Record> = {
editor: CodeEditorPage,
workflows: CodeEditorWorkflowsPage,
},
+ workspace: {
+ dashboard: WorkspacePage,
+ settings: WorkspaceSettingsPage,
+ },
teamsbot: {
dashboard: TeamsbotDashboardView,
sessions: TeamsbotSessionView,
@@ -199,6 +207,12 @@ export const FeatureViewPage: React.FC = ({ view }) => {
return ;
}
+ // Workspace dashboard is rendered persistently by WorkspaceKeepAlive at MainLayout level;
+ // other workspace views (e.g. settings) use the standard FeatureViewPage rendering.
+ if (featureCode === 'workspace' && view !== 'settings') {
+ return null;
+ }
+
// View-Komponente finden
const featureViews = VIEW_COMPONENTS[featureCode];
if (!featureViews) {
diff --git a/src/pages/admin/Admin.module.css b/src/pages/admin/Admin.module.css
index ccb0b9b..4fcc8df 100644
--- a/src/pages/admin/Admin.module.css
+++ b/src/pages/admin/Admin.module.css
@@ -88,6 +88,34 @@
border-color: var(--text-secondary);
}
+.googleButton {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 1rem;
+ background: #4285f4;
+ color: white;
+ border: none;
+ border-radius: 6px;
+ font-size: 0.875rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background 0.2s, transform 0.1s;
+}
+
+.googleButton:hover {
+ background: #3367d6;
+}
+
+.googleButton:active {
+ transform: scale(0.98);
+}
+
+.googleButton:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
/* Filter Section Styles */
.filterSection {
display: flex;
diff --git a/src/pages/basedata/ConnectionsPage.tsx b/src/pages/basedata/ConnectionsPage.tsx
index 0661fdb..9cd0b7e 100644
--- a/src/pages/basedata/ConnectionsPage.tsx
+++ b/src/pages/basedata/ConnectionsPage.tsx
@@ -221,7 +221,7 @@ export const ConnectionsPage: React.FC = () => {
{canCreate && (
<>