diff --git a/opax-web/backend/routes/consoleApi.js b/opax-web/backend/routes/consoleApi.js
index ca9129a..c18ae50 100644
--- a/opax-web/backend/routes/consoleApi.js
+++ b/opax-web/backend/routes/consoleApi.js
@@ -101,4 +101,13 @@ router.get('/capabilities', (req, res) => {
res.json(capabilities);
});
+router.get('/emma/status', async (req, res) => {
+ try {
+ await callAgent('get_health', {});
+ res.json({ status: 'ready', label: 'Emma Local', message: 'Emma is ready.' });
+ } catch (e) {
+ res.status(500).json({ status: 'unavailable', label: 'Emma Local', message: 'Emma is unavailable.' });
+ }
+});
+
export default router;
diff --git a/opax-web/frontend/src/components/AgentConsoleView.tsx b/opax-web/frontend/src/components/AgentConsoleView.tsx
deleted file mode 100644
index a38a591..0000000
--- a/opax-web/frontend/src/components/AgentConsoleView.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import React, { useState } from 'react';
-
-const AgentConsoleView = () => {
- const [message, setMessage] = useState('');
- const [transcript, setTranscript] = useState([]);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState(null);
- const [ticketNumber, setTicketNumber] = useState(null);
-
- const handleSend = async () => {
- if (!message.trim()) return;
-
- const newMessage = { role: 'user', content: message };
- setTranscript(prev => [...prev, newMessage]);
- setMessage('');
- setLoading(true);
- setError(null);
-
- try {
- const res = await fetch('/api/console/emma/chat', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- credentials: 'include',
- body: JSON.stringify({ message, ticket_number: ticketNumber }),
- });
-
- if (!res.ok) {
- throw new Error('Emma is unavailable.');
- }
-
- const data = await res.json();
- const emmaMessage = { role: 'assistant', content: data.reply };
- setTranscript(prev => [...prev, emmaMessage]);
-
- } catch (err) {
- setError(err.message);
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
Emma Local
-
- Local model · server-side memory enabled
- {ticketNumber && Working context: Gitea #{ticketNumber}}
-
-
- {transcript.map((msg, index) => (
-
- {msg.role}: {msg.content}
-
- ))}
- {loading &&
...
}
- {error &&
{error}
}
-
-
- setMessage(e.target.value)}
- onKeyPress={(e) => e.key === 'Enter' && handleSend()}
- placeholder="Chat with Emma..."
- disabled={loading}
- />
-
-
-
- );
-};
-
-export default AgentConsoleView;
diff --git a/opax-web/frontend/src/components/ApprovalsView.tsx b/opax-web/frontend/src/components/ApprovalsView.tsx
deleted file mode 100644
index 5075d7e..0000000
--- a/opax-web/frontend/src/components/ApprovalsView.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-
-const ApprovalsView = () => {
- return (
-
-
Approvals
-
No approval read model configured yet.
-
- );
-};
-
-export default ApprovalsView;
diff --git a/opax-web/frontend/src/components/CapabilityPanel.tsx b/opax-web/frontend/src/components/CapabilityPanel.tsx
index 3d6d30a..1430178 100644
--- a/opax-web/frontend/src/components/CapabilityPanel.tsx
+++ b/opax-web/frontend/src/components/CapabilityPanel.tsx
@@ -1,7 +1,16 @@
import React, { useState, useEffect } from 'react';
+interface Capability {
+ type: string;
+}
+
+interface Capabilities {
+ [key: string]: Capability;
+}
+
const CapabilityPanel = () => {
- const [capabilities, setCapabilities] = useState({});
+ const [capabilities, setCapabilities] = useState({});
+ const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
const fetchCapabilities = async () => {
@@ -13,35 +22,36 @@ const CapabilityPanel = () => {
}, []);
const capabilityGroups = {
- 'Platform Status': ['get_health', 'get_build_status', 'get_state', 'get_telemetry'],
- 'Git and Gitea': ['list_commits', 'get_file'],
- 'Tickets and Work Queue': [],
- 'Code Agents': ['run_emma'],
- 'Tests and Validation': [],
- 'Builds and Deployments': ['trigger_build', 'commit_and_push_files'],
- 'Cloud Run and Infrastructure': [],
- 'Logs and Telemetry': [],
- 'Billing and CostGuard': ['get_billing_summary', 'get_billing_forecast', 'get_billing_anomalies', 'get_billing_budget', 'set_billing_budget'],
- 'Customers and Workspace': ['list_customers', 'list_workspace_users', 'create_invite'],
+ 'Platform': ['get_health', 'get_build_status', 'get_state', 'get_telemetry'],
+ 'Repository': ['list_commits', 'get_file'],
+ 'Work and tickets': [],
+ 'Code agents': ['run_emma'],
+ 'Validation': [],
+ 'Deployment': ['trigger_build', 'commit_and_push_files'],
+ 'Costs': ['get_billing_summary', 'get_billing_forecast', 'get_billing_anomalies', 'get_billing_budget', 'set_billing_budget'],
'Notifications': ['send_email', 'send_sms', 'send_webhook'],
+ 'Customers and Workspace': ['list_customers', 'list_workspace_users', 'create_invite'],
'Memory and Knowledge': [],
};
return (
-
-
Capabilities
- {Object.entries(capabilityGroups).map(([group, capabilityKeys]) => (
-
-
{group}
-
- {capabilityKeys.map(key => {
- const capability = capabilities[key];
- if (!capability) return null;
- return - {key}
- })}
-
+
+
+
+
Capabilities
+ {Object.entries(capabilityGroups).map(([group, capabilityKeys]) => (
+
+
{group}
+
+ {capabilityKeys.map(key => {
+ const capability = capabilities[key];
+ if (!capability) return null;
+ return - {key.replace(/_/g, ' ')}
+ })}
+
+
+ ))}
- ))}
);
};
diff --git a/opax-web/frontend/src/components/ChatWorkspace.tsx b/opax-web/frontend/src/components/ChatWorkspace.tsx
new file mode 100644
index 0000000..670cf03
--- /dev/null
+++ b/opax-web/frontend/src/components/ChatWorkspace.tsx
@@ -0,0 +1,105 @@
+import React, { useState, useEffect } from 'react';
+
+interface Message {
+ role: string;
+ content: string;
+}
+
+const ChatWorkspace = () => {
+ const [message, setMessage] = useState('');
+ const [transcript, setTranscript] = useState
([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [emmaStatus, setEmmaStatus] = useState('unavailable');
+ const [ticketNumber, setTicketNumber] = useState(null);
+
+ useEffect(() => {
+ const fetchEmmaStatus = async () => {
+ try {
+ const res = await fetch('/api/console/emma/status', { credentials: 'include' });
+ const data = await res.json();
+ setEmmaStatus(data.status);
+ } catch (e) {
+ setEmmaStatus('unavailable');
+ }
+ };
+ fetchEmmaStatus();
+ }, []);
+
+ const handleSend = async () => {
+ if (!message.trim()) return;
+
+ const newMessage: Message = { role: 'user', content: message };
+ setTranscript(prev => [...prev, newMessage]);
+ setMessage('');
+ setLoading(true);
+ setError(null);
+
+ try {
+ const res = await fetch('/api/console/emma/chat', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ credentials: 'include',
+ body: JSON.stringify({ message, ticket_number: ticketNumber }),
+ });
+
+ if (!res.ok) {
+ throw new Error('Emma is unavailable.');
+ }
+
+ const data = await res.json();
+ const emmaMessage: Message = { role: 'assistant', content: data.reply };
+ setTranscript(prev => [...prev, emmaMessage]);
+
+ } catch (err: any) {
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
Emma Local
+ {emmaStatus === 'ready' ? 'Connected' : 'Unavailable'}
+ {ticketNumber && Gitea #{ticketNumber}}
+
+
+ {transcript.length === 0 && !loading && (
+
+
Hei Chris. Hva vil du at jeg skal få gjort?
+
+
+
+
+
+
+
+
+ )}
+ {transcript.map((msg, index) => (
+
+ {msg.role}: {msg.content}
+
+ ))}
+ {loading &&
...
}
+ {error &&
{error}
}
+
+
+
+
+ );
+};
+
+export default ChatWorkspace;
diff --git a/opax-web/frontend/src/components/ContextPanel.tsx b/opax-web/frontend/src/components/ContextPanel.tsx
new file mode 100644
index 0000000..74e2771
--- /dev/null
+++ b/opax-web/frontend/src/components/ContextPanel.tsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import CapabilityPanel from './CapabilityPanel';
+
+const ContextPanel = () => {
+ return (
+
+
+
+
Ticket context
+
No active ticket
+
+
+
Suggested actions
+
+ - Check platform
+ - Review Gitea #1
+ - Check last deployment
+
+
+
+
+ );
+};
+
+export default ContextPanel;
diff --git a/opax-web/frontend/src/components/PlatformStatus.tsx b/opax-web/frontend/src/components/PlatformStatus.tsx
deleted file mode 100644
index a33cdc4..0000000
--- a/opax-web/frontend/src/components/PlatformStatus.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import React from 'react';
-
-type ConsoleStatus = 'success' | 'unavailable' | 'loading';
-
-type SafeHealthSummary = {
- status: 'ok';
- service: string;
- version: string;
-};
-
-type ConsoleStatusItem = {
- status: Exclude;
- summary: SafeHealthSummary | null;
- message: 'Available' | 'Unavailable';
-};
-
-interface ConsoleStatusResponse {
- health?: ConsoleStatusItem;
- buildStatus?: ConsoleStatusItem;
- platformState?: ConsoleStatusItem;
-}
-
-interface StatusBlockProps {
- title: string;
- status: ConsoleStatus;
- summary: SafeHealthSummary | null;
-}
-
-const StatusBlock: React.FC = ({ title, status, summary }) => {
- return (
-
-
{title}
- {status === 'loading' &&
Loading...
}
- {status === 'unavailable' &&
Unavailable
}
- {status === 'success' && summary && (
-
- - Status: {summary.status}
- - Service: {summary.service}
- - Version: {summary.version}
-
- )}
-
- );
-};
-
-interface PlatformStatusProps {
- statusData: ConsoleStatusResponse | null;
- loading: boolean;
-}
-
-const unavailableStatus: ConsoleStatus = 'unavailable';
-
-const PlatformStatus: React.FC = ({ statusData, loading }) => {
- return (
-
-
Platform Overview
-
-
-
-
-
-
- );
-};
-
-export default PlatformStatus;
diff --git a/opax-web/frontend/src/components/ProjectRegistryView.tsx b/opax-web/frontend/src/components/ProjectRegistryView.tsx
deleted file mode 100644
index 945b196..0000000
--- a/opax-web/frontend/src/components/ProjectRegistryView.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-
-const ProjectRegistryView = () => {
- return (
-
-
Projects
-
No project registry configured yet.
-
- );
-};
-
-export default ProjectRegistryView;
diff --git a/opax-web/frontend/src/components/Sidebar.tsx b/opax-web/frontend/src/components/Sidebar.tsx
new file mode 100644
index 0000000..c60314e
--- /dev/null
+++ b/opax-web/frontend/src/components/Sidebar.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+
+const Sidebar = () => {
+ return (
+
+
OPAX
+
Emma Local: Connected
+
+
+
Current selected ticket:
+
No active task
+
+
User Profile
+
+ );
+};
+
+export default Sidebar;
diff --git a/opax-web/frontend/src/components/WorkQueueView.tsx b/opax-web/frontend/src/components/WorkQueueView.tsx
deleted file mode 100644
index c46374d..0000000
--- a/opax-web/frontend/src/components/WorkQueueView.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-
-const WorkQueueView = () => {
- return (
-
-
Work Queue
-
No ticket read model configured yet.
-
- );
-};
-
-export default WorkQueueView;
diff --git a/opax-web/frontend/src/pages/OperationsConsole.tsx b/opax-web/frontend/src/pages/OperationsConsole.tsx
index 6ccae6b..242dfd4 100644
--- a/opax-web/frontend/src/pages/OperationsConsole.tsx
+++ b/opax-web/frontend/src/pages/OperationsConsole.tsx
@@ -1,138 +1,14 @@
-import React, { useState, useEffect, useCallback } from 'react';
-import PlatformStatus from '../components/PlatformStatus';
-import ProjectRegistryView from '../components/ProjectRegistryView';
-import WorkQueueView from '../components/WorkQueueView';
-import ApprovalsView from '../components/ApprovalsView';
-import AgentConsoleView from '../components/AgentConsoleView';
-import CapabilityPanel from '../components/CapabilityPanel';
+import React from 'react';
+import Sidebar from '../components/Sidebar';
+import ChatWorkspace from '../components/ChatWorkspace';
+import ContextPanel from '../components/ContextPanel';
-// --- Types for Safe Data Handling ---
-type ConsoleStatus = 'success' | 'unavailable';
-
-type SafeHealthSummary = {
- status: 'ok';
- service: string;
- version: string;
-};
-
-type ConsoleStatusItem = {
- status: ConsoleStatus;
- summary: SafeHealthSummary | null;
- message: 'Available' | 'Unavailable';
-};
-
-export interface ConsoleStatusResponse {
- health: ConsoleStatusItem;
- buildStatus: ConsoleStatusItem;
- platformState: ConsoleStatusItem;
-}
-
-const unavailableItem: ConsoleStatusItem = {
- status: 'unavailable',
- summary: null,
- message: 'Unavailable',
-};
-
-// --- Normalizer Function ---
-type UnknownRecord = Record;
-
-function isRecord(value: unknown): value is UnknownRecord {
- return typeof value === 'object' && value !== null && !Array.isArray(value);
-}
-
-function isHealthSummary(value: unknown): value is SafeHealthSummary {
- return (
- isRecord(value) &&
- value.status === 'ok' &&
- typeof value.service === 'string' &&
- typeof value.version === 'string'
- );
-}
-
-function normalizeStatusResponse(raw: unknown): ConsoleStatusResponse {
- let health: ConsoleStatusItem = unavailableItem;
-
- if (isRecord(raw) && isRecord(raw.health)) {
- const healthItem = raw.health;
-
- if (
- healthItem.status === 'success' &&
- isHealthSummary(healthItem.summary)
- ) {
- health = {
- status: 'success',
- summary: {
- status: 'ok',
- service: healthItem.summary.service,
- version: healthItem.summary.version,
- },
- message: 'Available',
- };
- }
- }
-
- return {
- health,
- buildStatus: unavailableItem,
- platformState: unavailableItem,
- };
-}
-
-// --- Component ---
const OperationsConsole = () => {
- const [statusData, setStatusData] = useState(null);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const [lastRefreshed, setLastRefreshed] = useState('');
-
- const fetchData = useCallback(async () => {
- setLoading(true);
- setError(null);
- try {
- const res = await fetch('/api/console/status', { credentials: 'include' });
- if (!res.ok) {
- // Handle auth errors or server errors safely
- if (res.status === 401) {
- throw new Error('Access denied. Please log in again.');
- } else {
- throw new Error('Console data is unavailable.');
- }
- }
- const rawData = await res.json();
- setStatusData(normalizeStatusResponse(rawData));
- setLastRefreshed(new Date().toLocaleTimeString());
- } catch {
- setError('Console data is unavailable. Please try again.');
- } finally {
- setLoading(false);
- }
- }, []);
-
- useEffect(() => {
- fetchData();
- }, [fetchData]);
-
return (
-
-
-
OPAX Operations Console
-
- Last refreshed: {lastRefreshed || 'never'}
-
-
-
-
- {error &&
{error}
}
-
-
-
-
-
-
-
-
+
+
+
+
);
};