Compare commits

..

5 Commits

Author SHA1 Message Date
SamiAhmed7777 aff7263afd feat: Add container management plugin for OpenWebUI - Add monitoring, update, and restart capabilities 2025-03-09 23:29:06 -07:00
SamiAhmed7777 77269fe4b4 Moved branding files to new folder
Deploy to HuggingFace Spaces / deploy (push) Blocked by required conditions
Create and publish Docker images with specific build args / build-main-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-cuda-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / build-ollama-image (linux/arm64) (push) Waiting to run
Create and publish Docker images with specific build args / merge-main-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-cuda-images (push) Blocked by required conditions
Create and publish Docker images with specific build args / merge-ollama-images (push) Blocked by required conditions
Release / release (push) Failing after 1m18s
Deploy to HuggingFace Spaces / check-secret (push) Successful in 11s
Create and publish Docker images with specific build args / build-main-image (linux/amd64) (push) Failing after 3m51s
Create and publish Docker images with specific build args / build-cuda-image (linux/amd64) (push) Failing after 1m11s
Create and publish Docker images with specific build args / build-ollama-image (linux/amd64) (push) Failing after 1m5s
Python CI / Format Backend (3.11) (push) Failing after 1m22s
Frontend Build / Format & Build Frontend (push) Failing after 1m41s
Frontend Build / Frontend Unit Tests (push) Failing after 52s
Release to PyPI / release (push) Failing after 1m28s
2025-03-05 21:02:07 -08:00
SamiAhmed7777 89afded87e Moved branding files to OpenWebUI's new static assets location 2025-03-05 19:54:55 -08:00
SamiAhmed7777 08a5ff700e Updated graphics 2025-03-01 01:28:55 -08:00
SamiAhmed7777 edf57900dc Updated graphics 2025-03-01 00:22:21 -08:00
34 changed files with 640 additions and 9 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 306 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 312 KiB

@@ -0,0 +1,35 @@
<script>
import { onMount } from 'svelte';
let containerStatus = '';
let error = '';
onMount(async () => {
try {
const response = await fetch('/api/container/status');
const data = await response.json();
containerStatus = data.status;
} catch (err) {
error = 'Failed to fetch container status';
console.error(err);
}
});
</script>
<div class="container-status">
{#if error}
<p class="error">{error}</p>
{:else}
<p>Container Status: {containerStatus}</p>
{/if}
</div>
<style>
.container-status {
padding: 1rem;
}
.error {
color: red;
}
</style>
+77
View File
@@ -0,0 +1,77 @@
# OpenWebUI Container Management Plugin
This plugin adds container management capabilities to OpenWebUI, allowing you to monitor, update, and restart the container directly from the web interface.
## Features
- View container status and version
- Update container to the latest version
- Restart container when needed
- User-friendly interface integrated into OpenWebUI settings
## Installation
### Installing in Docker-based OpenWebUI
1. If your OpenWebUI is running in Docker, mount the plugin directory into the container by adding this volume to your docker-compose.yml:
```yaml
services:
openwebui:
volumes:
- ./custom:/app/custom # Mount the plugin directory
```
2. Copy the `custom` directory to your OpenWebUI installation directory (where your docker-compose.yml is located)
3. Restart your OpenWebUI container:
```bash
docker-compose restart openwebui
```
The plugin will be automatically detected and loaded by OpenWebUI when it starts.
### Dependencies
The plugin requires the following Python packages which are already included in the OpenWebUI Docker image:
- fastapi
- docker-py
## Usage
1. Navigate to the OpenWebUI settings page
2. Look for the "Container Management" section
3. You can:
- View the current container status and version
- Click "Update Container" to update to the latest version
- Click "Restart Container" to restart the container
## API Endpoints
The plugin exposes the following API endpoints:
- `GET /api/container/status` - Get container status and version
- `POST /api/container/update` - Update the container
- `POST /api/container/restart` - Restart the container
## Development
To modify or extend the plugin:
1. Frontend components are in `custom/components/`
2. Backend API routes are in `custom/api/`
3. Container management logic is in `custom/container_manager.py`
4. Plugin registration is handled in `custom/plugin.py`
### Development Workflow
1. Make changes to the plugin code
2. The changes will be reflected immediately in the OpenWebUI container due to the volume mount
3. If you modify Python files, you'll need to restart the OpenWebUI container:
```bash
docker-compose restart openwebui
```
4. Frontend changes (Svelte components) will be hot-reloaded automatically
## Contributing
Feel free to submit issues and pull requests to improve the plugin.
+43
View File
@@ -0,0 +1,43 @@
from fastapi import APIRouter, HTTPException
from typing import Dict
from ..container_manager import ContainerManager
router = APIRouter()
container_manager = ContainerManager()
@router.get("/status")
async def get_container_status() -> Dict:
"""Get the current status and version of the container."""
try:
status = container_manager.get_status()
version = container_manager.get_version()
return {
"status": status,
"version": version
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/update")
async def update_container() -> Dict:
"""Update the container to the latest version."""
try:
success = container_manager.update()
return {
"success": success,
"message": "Container updated successfully" if success else "Update failed"
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/restart")
async def restart_container() -> Dict:
"""Restart the container."""
try:
success = container_manager.restart()
return {
"success": success,
"message": "Container restarted successfully" if success else "Restart failed"
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
+121
View File
@@ -0,0 +1,121 @@
{/* ContainerSettings.svelte */}
<script lang="ts">
import { onMount } from 'svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Card } from '$lib/components/ui/card';
import { toast } from '$lib/components/ui/toaster';
let containerStatus = '';
let containerVersion = '';
let isUpdating = false;
let isRestarting = false;
async function fetchContainerStatus() {
try {
const response = await fetch('/api/container/status');
const data = await response.json();
containerStatus = data.status;
containerVersion = data.version;
} catch (error) {
toast({
title: 'Error',
description: 'Failed to fetch container status',
variant: 'destructive'
});
}
}
async function handleUpdate() {
isUpdating = true;
try {
const response = await fetch('/api/container/update', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
toast({
title: 'Success',
description: 'Container updated successfully'
});
await fetchContainerStatus();
} else {
throw new Error(data.message);
}
} catch (error) {
toast({
title: 'Error',
description: 'Failed to update container',
variant: 'destructive'
});
} finally {
isUpdating = false;
}
}
async function handleRestart() {
isRestarting = true;
try {
const response = await fetch('/api/container/restart', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
toast({
title: 'Success',
description: 'Container restarted successfully'
});
await fetchContainerStatus();
} else {
throw new Error(data.message);
}
} catch (error) {
toast({
title: 'Error',
description: 'Failed to restart container',
variant: 'destructive'
});
} finally {
isRestarting = false;
}
}
onMount(() => {
fetchContainerStatus();
});
</script>
<Card class="p-6">
<h2 class="text-2xl font-bold mb-4">Container Management</h2>
<div class="space-y-4">
<div class="flex flex-col gap-2">
<Label>Status</Label>
<Input value={containerStatus} readonly />
</div>
<div class="flex flex-col gap-2">
<Label>Version</Label>
<Input value={containerVersion} readonly />
</div>
<div class="flex gap-4 mt-4">
<Button
variant="primary"
disabled={isUpdating}
on:click={handleUpdate}
>
{isUpdating ? 'Updating...' : 'Update Container'}
</Button>
<Button
variant="secondary"
disabled={isRestarting}
on:click={handleRestart}
>
{isRestarting ? 'Restarting...' : 'Restart Container'}
</Button>
</div>
</div>
</Card>
+228
View File
@@ -0,0 +1,228 @@
"""
type: action
name: Container Manager
description: Manage chat.Sami container updates
version: 1.0.0
author: chat.Sami
"""
import os
import subprocess
import json
import requests
from typing import Dict, Any
class Action:
def __init__(self):
self.actions = [
{
"id": "update_container",
"name": "Update Container",
"description": "Update chat.Sami to the latest version"
},
{
"id": "container_status",
"name": "Container Status",
"description": "Get current container status and version"
},
{
"id": "check_updates",
"name": "Check Updates",
"description": "Check for available updates"
}
]
def execute(self, action_id: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
if action_id == "update_container":
return self._update_container()
elif action_id == "container_status":
return self._get_container_status()
elif action_id == "check_updates":
return self._check_updates()
else:
raise ValueError(f"Unknown action: {action_id}")
def _get_container_config(self) -> Dict[str, Any]:
"""Get current container configuration"""
try:
result = subprocess.run(
["docker", "inspect", "chat-sami"],
capture_output=True,
text=True,
check=True
)
container_info = json.loads(result.stdout)[0]
# Extract current configuration
config = {
"port": None,
"volumes": [],
"env": {}
}
# Get port mapping
ports = container_info["HostConfig"]["PortBindings"]
if "8080/tcp" in ports:
port_info = ports["8080/tcp"][0]
config["port"] = port_info["HostPort"]
# Get volume mappings
mounts = container_info["HostConfig"]["Mounts"]
for mount in mounts:
config["volumes"].append({
"source": mount["Source"],
"target": mount["Target"]
})
# Get environment variables
env_list = container_info["Config"]["Env"]
for env in env_list:
if "=" in env:
key, value = env.split("=", 1)
config["env"][key] = value
return config
except Exception as e:
return None
def _get_current_version(self) -> str:
try:
result = subprocess.run(
["docker", "inspect", "chat-sami"],
capture_output=True,
text=True,
check=True
)
container_info = json.loads(result.stdout)[0]
image_tag = container_info["Config"]["Image"].split(":")[-1]
return image_tag
except:
return "unknown"
def _get_latest_version(self) -> str:
try:
response = requests.get(
"https://api.github.com/repos/open-webui/open-webui/releases/latest",
timeout=5
)
if response.status_code == 200:
return response.json()["tag_name"].lstrip('v')
return "unknown"
except:
return "unknown"
def _check_updates(self) -> Dict[str, Any]:
current = self._get_current_version()
latest = self._get_latest_version()
return {
"success": True,
"data": {
"current_version": current,
"latest_version": latest,
"update_available": latest != "unknown" and current != latest
}
}
def _update_container(self) -> Dict[str, Any]:
try:
# Get current container configuration
config = self._get_container_config()
if not config:
raise Exception("Failed to get current container configuration")
# Pull the latest image
subprocess.run(
["docker", "pull", "ghcr.io/open-webui/open-webui:main"],
check=True,
capture_output=True
)
# Stop the current container
subprocess.run(
["docker", "stop", "chat-sami"],
check=True,
capture_output=True
)
# Remove the old container but keep the volumes
subprocess.run(
["docker", "rm", "chat-sami"],
check=True,
capture_output=True
)
# Create new container with same configuration
cmd = ["docker", "run", "-d", "--name", "chat-sami"]
# Add port mapping
if config["port"]:
cmd.extend(["-p", f"{config['port']}:8080"])
# Add volume mappings
for volume in config["volumes"]:
cmd.extend(["-v", f"{volume['source']}:{volume['target']}"])
# Add environment variables
for key, value in config["env"].items():
cmd.extend(["-e", f"{key}={value}"])
# Add image name
cmd.append("ghcr.io/open-webui/open-webui:main")
# Run the new container
subprocess.run(cmd, check=True, capture_output=True)
return {
"success": True,
"message": "Updated to latest version. The page will refresh in a few moments."
}
except subprocess.CalledProcessError as e:
return {
"success": False,
"message": f"Update failed: {e.stderr.decode() if e.stderr else str(e)}"
}
except Exception as e:
return {
"success": False,
"message": f"Error during update: {str(e)}"
}
def _get_container_status(self) -> Dict[str, Any]:
try:
# Get container info
result = subprocess.run(
["docker", "inspect", "chat-sami"],
capture_output=True,
text=True,
check=True
)
container_info = json.loads(result.stdout)[0]
# Get update status
update_info = self._check_updates()
current_version = update_info["data"]["current_version"]
latest_version = update_info["data"]["latest_version"]
update_available = update_info["data"]["update_available"]
return {
"success": True,
"data": {
"status": container_info["State"]["Status"],
"health": container_info["State"]["Health"]["Status"] if "Health" in container_info["State"] else "N/A",
"current_version": current_version,
"latest_version": latest_version,
"update_available": update_available,
"created": container_info["Created"]
}
}
except subprocess.CalledProcessError as e:
return {
"success": False,
"message": f"Failed to get container status: {e.stderr.decode() if e.stderr else str(e)}"
}
except Exception as e:
return {
"success": False,
"message": f"Error: {str(e)}"
}
+66
View File
@@ -0,0 +1,66 @@
"""
type: settings
name: Container Settings
description: Manage chat.Sami container settings
version: 1.0.0
author: chat.Sami
"""
from typing import Dict, Any
import json
from fastapi import APIRouter
from pydantic import BaseModel
router = APIRouter()
class ContainerSettings(BaseModel):
"""Container management settings."""
enabled: bool = True
@router.get("/settings/container")
async def get_container_settings() -> Dict[str, Any]:
"""Get container management settings."""
return {
"id": "container",
"title": "Container Management",
"description": "Manage the chat.Sami container",
"component": "ContainerStatus",
"settings": ContainerSettings().dict()
}
class Settings:
def __init__(self):
self.settings = {
"container": {
"title": "Container Management",
"description": "Manage your chat.Sami container",
"type": "object",
"properties": {
"status": {
"type": "custom",
"component": "ContainerStatus",
"action": "container_status",
"refresh_interval": 30, # Refresh every 30 seconds
"title": "Container Status"
},
"update": {
"type": "button",
"title": "Update Container",
"description": "Update chat.Sami to the latest version",
"action": "update_container",
"buttonText": "Update Now",
"confirmText": "Are you sure you want to update the container? The service will restart briefly."
}
}
}
}
def get_settings(self) -> Dict[str, Any]:
return self.settings
def update_settings(self, settings: Dict[str, Any]) -> Dict[str, Any]:
# This is a read-only settings panel
return {
"success": True,
"message": "Settings updated"
}
+26
View File
@@ -0,0 +1,26 @@
export async function executeAction<T>(actionId: string, params?: Record<string, unknown>): Promise<T> {
// This is a mock implementation for testing
const mockResponses = {
container_status: {
success: true,
data: {
status: "running",
health: "healthy",
current_version: "1.0.0",
latest_version: "1.1.0",
update_available: true,
created: new Date().toISOString()
}
},
update_container: {
success: true,
message: "Update initiated"
}
};
return new Promise((resolve) => {
setTimeout(() => {
resolve(mockResponses[actionId as keyof typeof mockResponses] as T);
}, 500);
});
}
+23
View File
@@ -0,0 +1,23 @@
from fastapi import FastAPI
from .api.container_routes import router as container_router
def register_plugin(app: FastAPI) -> None:
"""Register the container management plugin with OpenWebUI."""
# Register API routes
app.include_router(
container_router,
prefix="/api/container",
tags=["container"]
)
# Register frontend components
app.state.plugin_components = app.state.plugin_components or {}
app.state.plugin_components["container_settings"] = {
"name": "ContainerSettings",
"path": "custom/components/ContainerSettings.svelte",
"settings": {
"title": "Container Management",
"icon": "container",
"description": "Manage container updates and status"
}
}
+14
View File
@@ -0,0 +1,14 @@
export interface ContainerStatus {
status: string;
health: string;
current_version: string;
latest_version: string;
update_available: boolean;
created: string;
}
export interface ContainerResponse {
success: boolean;
data?: ContainerStatus;
message?: string;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 306 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 306 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 409 KiB

+5 -5
View File
@@ -1,21 +1,21 @@
{
"name": "Open WebUI",
"short_name": "WebUI",
"name": "chat.sami",
"short_name": "chat.sami",
"icons": [
{
"src": "/static/web-app-manifest-192x192.png",
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/static/web-app-manifest-512x512.png",
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"theme_color": "#076291",
"background_color": "#ffffff",
"display": "standalone"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 339 KiB