Skip to content

Commit b3ef977

Browse files
committed
feat: [wip] Open WebUI manipulation
1 parent b6e6e10 commit b3ef977

File tree

8 files changed

+289
-4
lines changed

8 files changed

+289
-4
lines changed

Dockerfile

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,37 @@
1-
FROM node:22.16.0-alpine3.22 AS base
1+
FROM node:22-slim AS base
22

33
# Install bash & curl for entrypoint script compatibility
4-
RUN apk add --no-cache bash curl
4+
# as well as dependencies for Playwright Chromium
5+
RUN apt-get update && apt-get install -y \
6+
bash \
7+
curl \
8+
wget \
9+
ca-certificates \
10+
fonts-liberation \
11+
libnss3 \
12+
libatk-bridge2.0-0 \
13+
libdrm2 \
14+
libxkbcommon0 \
15+
libgbm1 \
16+
libasound2 \
17+
libxcb-shm0 \
18+
libx11-xcb1 \
19+
libxrandr2 \
20+
libxcomposite1 \
21+
libxcursor1 \
22+
libxdamage1 \
23+
libxfixes3 \
24+
libxi6 \
25+
libgtk-3-0t64 \
26+
libpangocairo-1.0-0 \
27+
libpango-1.0-0 \
28+
libatk1.0-0t64 \
29+
libcairo-gobject2 \
30+
libcairo2 \
31+
libgdk-pixbuf-2.0-0 \
32+
libxrender1 \
33+
libasound2t64
34+
&& rm -rf /var/lib/apt/lists/*
535

636
# All deps stage
737
FROM base AS deps

admin/app/controllers/settings_controller.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { MapService } from '#services/map_service';
2+
import { OpenWebUIService } from '#services/openwebui_service';
23
import { SystemService } from '#services/system_service';
34
import { inject } from '@adonisjs/core';
45
import type { HttpContext } from '@adonisjs/core/http'
@@ -7,7 +8,8 @@ import type { HttpContext } from '@adonisjs/core/http'
78
export default class SettingsController {
89
constructor(
910
private systemService: SystemService,
10-
private mapService: MapService
11+
private mapService: MapService,
12+
private openWebUIService: OpenWebUIService
1113
) { }
1214

1315
async system({ inertia }: HttpContext) {
@@ -43,6 +45,15 @@ export default class SettingsController {
4345
});
4446
}
4547

48+
async models({ inertia }: HttpContext) {
49+
const installedModels = await this.openWebUIService.getInstalledModels();
50+
return inertia.render('settings/models', {
51+
models: {
52+
installedModels: installedModels || []
53+
}
54+
});
55+
}
56+
4657
async update({ inertia }: HttpContext) {
4758
const updateInfo = await this.systemService.checkLatestVersion();
4859
return inertia.render('settings/update', {
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { inject } from '@adonisjs/core'
2+
import { chromium } from 'playwright'
3+
import { SystemService } from './system_service.js'
4+
import logger from '@adonisjs/core/services/logger'
5+
import { DockerService } from './docker_service.js'
6+
import { ServiceSlim } from '../../types/services.js'
7+
import axios from 'axios'
8+
9+
@inject()
10+
export class OpenWebUIService {
11+
constructor(private systemService: SystemService) {}
12+
13+
async getOpenWebUIToken(): Promise<{
14+
token: string
15+
location: string
16+
} | null> {
17+
try {
18+
const { openWebUIService } = await this.getOpenWebUIAndOllamaServices()
19+
if (!openWebUIService) {
20+
logger.warn('[OpenWebUIService] Open WebUI service is not installed.')
21+
return null
22+
}
23+
24+
const location = this.extractOpenWebUIUrl(openWebUIService)
25+
if (!location) {
26+
logger.warn('[OpenWebUIService] Could not determine Open WebUI URL.')
27+
return null
28+
}
29+
30+
const browser = await chromium.launch({ headless: true })
31+
const context = await browser.newContext()
32+
const page = await context.newPage()
33+
34+
try {
35+
await page.goto(location, { waitUntil: 'networkidle' })
36+
37+
const cookies = await context.cookies()
38+
const tokenCookie = cookies.find((cookie) => cookie.name === 'token')
39+
if (tokenCookie) {
40+
return { token: tokenCookie.value, location }
41+
}
42+
43+
return null
44+
} finally {
45+
await browser.close()
46+
}
47+
} catch (error) {
48+
logger.error(
49+
`[OpenWebUIService] Failed to get Open WebUI token: ${error instanceof Error ? error.message : error}`
50+
)
51+
return null
52+
}
53+
}
54+
55+
async getInstalledModels(): Promise<string[] | null> {
56+
try {
57+
const tokenData = await this.getOpenWebUIToken()
58+
if (!tokenData) {
59+
logger.warn('[OpenWebUIService] Cannot get installed models without Open WebUI token.')
60+
return null
61+
}
62+
63+
const response = await axios.get(tokenData.location + '/ollama/api/tags', {
64+
headers: {
65+
Authorization: `Bearer ${tokenData.token}`,
66+
},
67+
})
68+
69+
if (response.status === 200 && response.data.models && Array.isArray(response.data.models)) {
70+
console.log("GOT RESPONSE DATA:", response.data)
71+
return response.data.models as string[]
72+
}
73+
74+
logger.warn(
75+
`[OpenWebUIService] Unexpected response when fetching installed models: ${response.status}`
76+
)
77+
return null
78+
} catch (error) {
79+
logger.error(
80+
`[OpenWebUIService] Failed to get installed models: ${error instanceof Error ? error.message : error}`
81+
)
82+
return null
83+
}
84+
}
85+
86+
private async getOpenWebUIAndOllamaServices(): Promise<{
87+
openWebUIService: ServiceSlim | null
88+
ollamaService: ServiceSlim | null
89+
}> {
90+
try {
91+
const services = await this.systemService.getServices({ installedOnly: true })
92+
93+
const owuiContainer = services.find(
94+
(service) => service.service_name === DockerService.OPEN_WEBUI_SERVICE_NAME
95+
)
96+
const ollamaContainer = services.find(
97+
(service) => service.service_name === DockerService.OLLAMA_SERVICE_NAME
98+
)
99+
100+
return {
101+
openWebUIService: owuiContainer || null,
102+
ollamaService: ollamaContainer || null,
103+
}
104+
} catch (error) {
105+
logger.error(
106+
`[OpenWebUIService] Failed to get Open WebUI and Ollama services: ${error instanceof Error ? error.message : error}`
107+
)
108+
return {
109+
openWebUIService: null,
110+
ollamaService: null,
111+
}
112+
}
113+
}
114+
115+
private extractOpenWebUIUrl(service: ServiceSlim): string | null {
116+
const location = service.ui_location || '3000'
117+
if (!location || isNaN(Number(location))) {
118+
logger.warn(`[OpenWebUIService] Invalid Open WebUI location: ${location}`)
119+
return null
120+
}
121+
return `http://localhost:${location}`
122+
}
123+
}

admin/inertia/layouts/SettingsLayout.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,21 @@ import {
44
FolderIcon,
55
MagnifyingGlassIcon,
66
} from '@heroicons/react/24/outline'
7-
import { IconArrowBigUpLines, IconDashboard, IconGavel, IconMapRoute } from '@tabler/icons-react'
7+
import {
8+
IconArrowBigUpLines,
9+
IconDashboard,
10+
IconDatabaseStar,
11+
IconGavel,
12+
IconMapRoute,
13+
} from '@tabler/icons-react'
814
import StyledSidebar from '~/components/StyledSidebar'
915
import { getServiceLink } from '~/lib/navigation'
1016

1117
const navigation = [
1218
{ name: 'Apps', href: '/settings/apps', icon: CommandLineIcon, current: false },
1319
{ name: 'Legal Notices', href: '/settings/legal', icon: IconGavel, current: false },
1420
{ name: 'Maps Manager', href: '/settings/maps', icon: IconMapRoute, current: false },
21+
{ name: 'Models Manager', href: '/settings/models', icon: IconDatabaseStar, current: false },
1522
{
1623
name: 'Service Logs & Metrics',
1724
href: getServiceLink('9999'),
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Head } from '@inertiajs/react'
2+
import StyledTable from '~/components/StyledTable'
3+
import SettingsLayout from '~/layouts/SettingsLayout'
4+
import { ServiceSlim } from '../../../types/services'
5+
import { getServiceLink } from '~/lib/navigation'
6+
import LoadingSpinner from '~/components/LoadingSpinner'
7+
import { IconCheck } from '@tabler/icons-react'
8+
import { useState } from 'react'
9+
10+
export default function ModelsPage(props: { models: { installedModels: string[] } }) {
11+
const [loading, setLoading] = useState(false)
12+
13+
return (
14+
<SettingsLayout>
15+
<Head title="App Settings" />
16+
<div className="xl:pl-72 w-full">
17+
<main className="px-12 py-6">
18+
<h1 className="text-4xl font-semibold mb-4">Models</h1>
19+
<p className="text-gray-500 mb-4">Easily manage the AI models available for Open WebUI</p>
20+
{loading && <LoadingSpinner fullscreen />}
21+
{!loading && (
22+
<StyledTable<ServiceSlim & { actions?: any }>
23+
className="font-semibold"
24+
rowLines={true}
25+
columns={[
26+
{
27+
accessor: 'friendly_name',
28+
title: 'Name',
29+
render(record) {
30+
return (
31+
<div className="flex flex-col">
32+
<p>{record.friendly_name || record.service_name}</p>
33+
<p className="text-sm text-gray-500">{record.description}</p>
34+
</div>
35+
)
36+
},
37+
},
38+
{
39+
accessor: 'ui_location',
40+
title: 'Port',
41+
render: (record) => (
42+
<a
43+
href={getServiceLink(record.ui_location || 'unknown')}
44+
target="_blank"
45+
rel="noopener noreferrer"
46+
className="text-desert-green hover:underline font-semibold"
47+
>
48+
{record.ui_location}
49+
</a>
50+
),
51+
},
52+
{
53+
accessor: 'installed',
54+
title: 'Installed',
55+
render: (record) =>
56+
record.installed ? <IconCheck className="h-6 w-6 text-desert-green" /> : '',
57+
},
58+
]}
59+
data={[]}
60+
/>
61+
)}
62+
</main>
63+
</div>
64+
</SettingsLayout>
65+
)
66+
}

admin/package-lock.json

Lines changed: 45 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

admin/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"scripts": {
99
"start": "node bin/server.js",
1010
"build": "node ace build",
11+
"postinstall": "playwright install chromium --with-deps",
1112
"dev": "node ace serve --hmr",
1213
"test": "node ace test",
1314
"lint": "eslint .",
@@ -91,6 +92,7 @@
9192
"maplibre-gl": "^4.7.1",
9293
"mysql2": "^3.14.1",
9394
"pino-pretty": "^13.0.0",
95+
"playwright": "^1.57.0",
9496
"pmtiles": "^4.3.0",
9597
"postcss": "^8.5.6",
9698
"react": "^19.1.0",

admin/start/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ router
3333
router.get('/apps', [SettingsController, 'apps'])
3434
router.get('/legal', [SettingsController, 'legal'])
3535
router.get('/maps', [SettingsController, 'maps'])
36+
router.get('/models', [SettingsController, 'models'])
3637
router.get('/update', [SettingsController, 'update'])
3738
router.get('/zim', [SettingsController, 'zim'])
3839
router.get('/zim/remote-explorer', [SettingsController, 'zimRemote'])

0 commit comments

Comments
 (0)