- matrix-gateway: POST /internal/matrix/presence/online endpoint - usePresenceHeartbeat hook with activity tracking - Auto away after 5 min inactivity - Offline on page close/visibility change - Integrated in MatrixChatRoom component
13 KiB
13 KiB
Swapper Service - Cabinet Integration Guide
Version: 1.0.0
Last Updated: 2025-11-22
Status: ✅ Ready for Integration
Overview
This document describes how to integrate Swapper Service status and metrics into Node #1 and Node #2 admin consoles (cabinet interfaces).
API Endpoints for Cabinets
1. GET /api/cabinet/swapper/status
Get complete Swapper Service status for cabinet display.
Response:
{
"service": "swapper-service",
"status": "healthy",
"mode": "single-active",
"active_model": {
"name": "deepseek-r1-70b",
"uptime_hours": 1.5,
"request_count": 42,
"loaded_at": "2025-11-22T10:30:00"
},
"total_models": 8,
"available_models": ["deepseek-r1-70b", "qwen2.5-coder-32b", ...],
"loaded_models": ["deepseek-r1-70b"],
"models": [
{
"name": "deepseek-r1-70b",
"ollama_name": "deepseek-r1:70b",
"type": "llm",
"size_gb": 42,
"priority": "high",
"status": "loaded",
"is_active": true,
"uptime_hours": 1.5,
"request_count": 42,
"total_uptime_seconds": 5400.0
}
],
"timestamp": "2025-11-22T12:00:00"
}
2. GET /api/cabinet/swapper/models
Get detailed information about all models.
Response:
{
"models": [
{
"name": "deepseek-r1-70b",
"ollama_name": "deepseek-r1:70b",
"type": "llm",
"size_gb": 42,
"priority": "high",
"status": "loaded",
"is_active": true,
"can_load": false,
"can_unload": true,
"uptime_hours": 1.5,
"request_count": 42,
"total_uptime_seconds": 5400.0,
"loaded_at": "2025-11-22T10:30:00"
}
],
"total": 8,
"active_count": 1,
"timestamp": "2025-11-22T12:00:00"
}
3. GET /api/cabinet/swapper/metrics/summary
Get summary metrics for dashboard.
Response:
{
"summary": {
"total_models": 8,
"active_models": 1,
"available_models": 8,
"total_uptime_hours": 24.5,
"total_requests": 150
},
"most_used_model": {
"name": "deepseek-r1-70b",
"uptime_hours": 12.3,
"request_count": 85
},
"active_model": {
"name": "deepseek-r1-70b",
"uptime_hours": 1.5
},
"timestamp": "2025-11-22T12:00:00"
}
Frontend Integration Examples
React Component Example
import React, { useEffect, useState } from 'react';
interface SwapperStatus {
service: string;
status: string;
mode: string;
active_model: {
name: string;
uptime_hours: number;
request_count: number;
loaded_at: string;
} | null;
total_models: number;
models: Array<{
name: string;
type: string;
size_gb: number;
status: string;
is_active: boolean;
uptime_hours: number;
}>;
}
export const SwapperStatusCard: React.FC = () => {
const [status, setStatus] = useState<SwapperStatus | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchStatus = async () => {
try {
const response = await fetch('http://localhost:8890/api/cabinet/swapper/status');
const data = await response.json();
setStatus(data);
} catch (error) {
console.error('Error fetching Swapper status:', error);
} finally {
setLoading(false);
}
};
fetchStatus();
const interval = setInterval(fetchStatus, 30000); // Update every 30 seconds
return () => clearInterval(interval);
}, []);
if (loading) return <div>Loading...</div>;
if (!status) return <div>Error loading status</div>;
return (
<div className="swapper-status-card">
<h3>Swapper Service</h3>
<div className="status-info">
<p>Status: <span className={status.status}>{status.status}</span></p>
<p>Mode: {status.mode}</p>
<p>Total Models: {status.total_models}</p>
</div>
{status.active_model && (
<div className="active-model">
<h4>Active Model</h4>
<p>Name: {status.active_model.name}</p>
<p>Uptime: {status.active_model.uptime_hours.toFixed(2)} hours</p>
<p>Requests: {status.active_model.request_count}</p>
</div>
)}
<div className="models-list">
<h4>Available Models</h4>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Size (GB)</th>
<th>Status</th>
<th>Uptime (hours)</th>
</tr>
</thead>
<tbody>
{status.models.map((model) => (
<tr key={model.name} className={model.is_active ? 'active' : ''}>
<td>{model.name}</td>
<td>{model.type}</td>
<td>{model.size_gb}</td>
<td>{model.status}</td>
<td>{model.uptime_hours.toFixed(2)}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};
Vue Component Example
<template>
<div class="swapper-status-card">
<h3>Swapper Service</h3>
<div class="status-info">
<p>Status: <span :class="status.status">{{ status.status }}</span></p>
<p>Mode: {{ status.mode }}</p>
<p>Total Models: {{ status.total_models }}</p>
</div>
<div v-if="status.active_model" class="active-model">
<h4>Active Model</h4>
<p>Name: {{ status.active_model.name }}</p>
<p>Uptime: {{ status.active_model.uptime_hours.toFixed(2) }} hours</p>
<p>Requests: {{ status.active_model.request_count }}</p>
</div>
<div class="models-list">
<h4>Available Models</h4>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Size (GB)</th>
<th>Status</th>
<th>Uptime (hours)</th>
</tr>
</thead>
<tbody>
<tr
v-for="model in status.models"
:key="model.name"
:class="{ active: model.is_active }"
>
<td>{{ model.name }}</td>
<td>{{ model.type }}</td>
<td>{{ model.size_gb }}</td>
<td>{{ model.status }}</td>
<td>{{ model.uptime_hours.toFixed(2) }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
interface SwapperStatus {
service: string;
status: string;
mode: string;
active_model: {
name: string;
uptime_hours: number;
request_count: number;
} | null;
total_models: number;
models: Array<{
name: string;
type: string;
size_gb: number;
status: string;
is_active: boolean;
uptime_hours: number;
}>;
}
const status = ref<SwapperStatus | null>(null);
const loading = ref(true);
const fetchStatus = async () => {
try {
const response = await fetch('http://localhost:8890/api/cabinet/swapper/status');
const data = await response.json();
status.value = data;
} catch (error) {
console.error('Error fetching Swapper status:', error);
} finally {
loading.value = false;
}
};
onMounted(() => {
fetchStatus();
const interval = setInterval(fetchStatus, 30000);
onUnmounted(() => clearInterval(interval));
});
</script>
Dashboard Widgets
Widget 1: Active Model Display
<Widget title="Active Model">
{activeModel ? (
<div>
<h4>{activeModel.name}</h4>
<p>Uptime: {activeModel.uptime_hours.toFixed(2)} hours</p>
<p>Requests: {activeModel.request_count}</p>
<p>Loaded: {new Date(activeModel.loaded_at).toLocaleString()}</p>
</div>
) : (
<p>No model loaded</p>
)}
</Widget>
Widget 2: Model List with Actions
<Widget title="Models">
<table>
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>Uptime</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{models.map(model => (
<tr key={model.name}>
<td>{model.name}</td>
<td>
<Badge status={model.status}>
{model.is_active ? 'Active' : model.status}
</Badge>
</td>
<td>{model.uptime_hours.toFixed(2)}h</td>
<td>
{model.can_load && (
<Button onClick={() => loadModel(model.name)}>
Load
</Button>
)}
{model.can_unload && (
<Button onClick={() => unloadModel(model.name)}>
Unload
</Button>
)}
</td>
</tr>
))}
</tbody>
</table>
</Widget>
Widget 3: Metrics Summary
<Widget title="Metrics Summary">
<div className="metrics-grid">
<MetricCard
label="Total Models"
value={summary.total_models}
/>
<MetricCard
label="Active Models"
value={summary.active_models}
/>
<MetricCard
label="Total Uptime"
value={`${summary.total_uptime_hours.toFixed(2)}h`}
/>
<MetricCard
label="Total Requests"
value={summary.total_requests}
/>
</div>
{mostUsedModel && (
<div className="most-used">
<h4>Most Used Model</h4>
<p>{mostUsedModel.name}</p>
<p>{mostUsedModel.uptime_hours.toFixed(2)} hours</p>
</div>
)}
</Widget>
Integration Steps
Step 1: Add API Client
Create a service to fetch Swapper data:
// services/swapperService.ts
export const swapperService = {
async getStatus() {
const response = await fetch('http://localhost:8890/api/cabinet/swapper/status');
return response.json();
},
async getModels() {
const response = await fetch('http://localhost:8890/api/cabinet/swapper/models');
return response.json();
},
async getMetricsSummary() {
const response = await fetch('http://localhost:8890/api/cabinet/swapper/metrics/summary');
return response.json();
},
async loadModel(modelName: string) {
const response = await fetch(
`http://localhost:8890/models/${modelName}/load`,
{ method: 'POST' }
);
return response.json();
},
async unloadModel(modelName: string) {
const response = await fetch(
`http://localhost:8890/models/${modelName}/unload`,
{ method: 'POST' }
);
return response.json();
}
};
Step 2: Add to Admin Console
Add Swapper section to admin console sidebar:
// Admin Console Sidebar
const menuItems = [
{ id: 'overview', label: 'Overview', icon: 'dashboard' },
{ id: 'members', label: 'Members & Roles', icon: 'users' },
{ id: 'agents', label: 'Agents', icon: 'robot' },
{ id: 'swapper', label: 'Swapper Service', icon: 'swap' }, // Add this
{ id: 'settings', label: 'Settings', icon: 'settings' },
];
Step 3: Create Swapper Page
// pages/SwapperPage.tsx
export const SwapperPage: React.FC = () => {
return (
<div className="swapper-page">
<PageHeader title="Swapper Service" />
<div className="swapper-grid">
<SwapperStatusCard />
<SwapperMetricsSummary />
<SwapperModelsList />
</div>
</div>
);
};
Node-Specific Configuration
Node #1 (Production Server)
const SWAPPER_URL = 'http://swapper-service:8890'; // Internal Docker network
Node #2 (MacBook Development)
const SWAPPER_URL = 'http://localhost:8890'; // Local development
Error Handling
try {
const status = await swapperService.getStatus();
// Handle success
} catch (error) {
if (error.status === 503) {
// Service unavailable
showError('Swapper Service is not available');
} else if (error.status === 404) {
// Model not found
showError('Model not found');
} else {
// Generic error
showError('Error loading Swapper status');
}
}
Real-time Updates
Use polling or WebSocket for real-time updates:
// Polling example
useEffect(() => {
const interval = setInterval(async () => {
const status = await swapperService.getStatus();
setStatus(status);
}, 30000); // Update every 30 seconds
return () => clearInterval(interval);
}, []);
Styling Recommendations
.swapper-status-card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.active-model {
background: #e8f5e9;
padding: 15px;
border-radius: 4px;
margin: 15px 0;
}
.models-list table {
width: 100%;
border-collapse: collapse;
}
.models-list tr.active {
background: #fff3e0;
}
.status-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
.status-badge.loaded {
background: #4caf50;
color: white;
}
.status-badge.unloaded {
background: #9e9e9e;
color: white;
}
Testing
// Test Swapper API integration
describe('Swapper Service Integration', () => {
it('should fetch status', async () => {
const status = await swapperService.getStatus();
expect(status).toHaveProperty('service', 'swapper-service');
expect(status).toHaveProperty('status', 'healthy');
});
it('should load model', async () => {
const result = await swapperService.loadModel('deepseek-r1-70b');
expect(result.status).toBe('success');
});
});
Last Updated: 2025-11-22
Maintained by: Ivan Tytar & DAARION Team
Status: ✅ Ready for Integration