docs(readme): redesenha o README com hero em SVG [skip ci]

This commit is contained in:
evandro committed 2026-10-24 04:57:54 -03:00
1 parent 6766c27ca3
commit f4920d7ffc
2 files changed
+122 -119

No files matched your search

+78 -119
View File
@@ -1,111 +1,96 @@
<div align="center"> <div align="center">
# OllaHubBackend <img src="assets/readme/hero.svg" width="100%" alt="OllaHubBackend: API HTTP e WebSocket em Rust para instalar e operar o Ollama, com rotas protegidas por JWT">
**API HTTP + WebSocket em Rust (Actix Web) para gerenciar o Ollama: verifica e instala o Ollama, lista e baixa ![Rust 2021](https://img.shields.io/badge/Rust-edi%C3%A7%C3%A3o_2021-dea584?style=flat-square&logo=rust&logoColor=white)
modelos, faz chat com streaming e oferece criptografia AES-256-GCM, com JWT e Swagger UI.** ![Actix Web 4.11](https://img.shields.io/badge/Actix_Web-4.11-1447e6?style=flat-square)
![Versão 0.1.0](https://img.shields.io/badge/vers%C3%A3o-0.1.0-555?style=flat-square)
![Licença GPL-3.0](https://img.shields.io/badge/licen%C3%A7a-GPL--3.0-00bc7d?style=flat-square)
![Rust](https://img.shields.io/badge/Rust-edition_2021-informational) [Referência da API](docs/API.md) · [Exemplos de cliente](docs/API_EXAMPLES.md) · [Issues](https://git.evandro.dev.br/evandro/OllaHubBackend/issues)
![Actix Web](https://img.shields.io/badge/Actix_Web-4.11-informational)
![Licença](https://img.shields.io/badge/licen%C3%A7a-GPL--3.0-informational)
[Endpoints](#endpoints) · [Como funciona](#como-funciona) · [Configuração](#configura%C3%A7%C3%A3o) · [Executar](#executar-localmente) · [Limitações](#limita%C3%A7%C3%B5es-conhecidas)
</div> </div>
> [!NOTE] ## O que é
> **Estado do projeto:** versão `0.1.0` (`Cargo.toml`), com 3 commits entre 2025-11-05 e 2025-11-18 — estrutura
> inicial com API v2 modular, WebSocket, serviços de instalação do Ollama e documentação. A referência completa Um servidor HTTP e WebSocket em Rust ([Actix Web](https://actix.rs)) que cuida do [Ollama](https://ollama.com) por você: verifica se ele está instalado e respondendo, instala quando falta, lista e baixa modelos de um catálogo e conversa com eles por WebSocket. Traz também criptografia AES-256-GCM e um CRUD de usuários, com JWT e Swagger UI. O `.env.example` indica que os tokens vêm de um frontend separado.
> dos endpoints está em [`docs/API.md`](docs/API.md) e exemplos de cliente em
> [`docs/API_EXAMPLES.md`](docs/API_EXAMPLES.md). ## Por que usar
- **Setup do Ollama num lugar só.** `GET /v2/api/ollama/check` diz se o Ollama está no `PATH`, qual a versão e se a API responde (`reachable`, `unreachable` ou `degraded`). `POST /v2/api/ollama/install` instala por sistema: no Windows baixa o `OllamaSetup.exe` e roda `/silent`, no Linux usa o script oficial e no macOS extrai o zip em `/Applications`.
- **Chat que prepara o modelo.** Em `/v2/ws/chat`, se o modelo pedido não estiver instalado, o servidor o baixa antes de responder e avisa o progresso por mensagens de status.
- **Contrato documentado pelo código.** O OpenAPI é gerado com [utoipa](https://github.com/juhaku/utoipa) e servido no Swagger UI em `/docs`.
**Quando não usar:** exposto na internet ou em produção. Rotas que baixam e executam instaladores são públicas, os usuários ficam só em memória e o chat não faz streaming incremental. Se você só precisa conversar com um Ollama já instalado, a API dele na porta `11434` basta.
> [!IMPORTANT] > [!IMPORTANT]
> **Nem toda rota exige JWT.** O middleware só protege `/api/v1/*`, `/v2/api/crypto*` e `/v2/api/models*`. > **Nem toda rota exige JWT.** O middleware só protege `/api/v1/*`, `/v2/api/crypto*` e `/v2/api/models*`. Ficam **públicas** `/health`, `/v2/health`, `/v2/api/ollama/*` (inclusive `POST /v2/api/ollama/install`, que baixa e executa o instalador do Ollama), `/docs` e os WebSockets `/v2/ws*`. Por padrão o servidor escuta só em `127.0.0.1`; só mude `HOST` sabendo disso. O `.env.example` vem com um `JWT_SECRET` preenchido (sem ele, o padrão é `changeme-dev-secret`): troque pelo seu.
> Ficam **públicas** — inclusive `POST /v2/api/ollama/install`, que baixa e executa o instalador do Ollama —
> `/health`, `/v2/health`, `/v2/api/ollama/*`, `/docs` e os WebSockets `/v2/ws*`. Por padrão o servidor escuta
> só em `127.0.0.1`; defina `JWT_SECRET` com um valor próprio (o padrão é `changeme-dev-secret`) e só mude
> `HOST` sabendo disso.
## Endpoints ## Como funciona
Base URL padrão: `http://127.0.0.1:8080`. Swagger UI em `/docs` (OpenAPI em `/api-doc/openapi.json`). - `src/main.rs`, `src/config.rs` e `src/state.rs`: sobem o servidor, leem a configuração das variáveis de ambiente e montam o `AppContext` (eventos, criptografia, URL do Ollama, catálogo, segredo JWT).
- `src/api/`: rotas v2 de saúde, modelos, Ollama e criptografia.
- `src/core/models.rs`: lê o `modelList.json` (93 modelos; a API expõe `name` e `description`), consulta `/api/tags` e baixa com `/api/pull` do Ollama.
- `src/services/install/`: instalação do Ollama por sistema operacional.
- `src/ws/`: WebSockets de eventos (`/v2/ws`) e de chat (`/v2/ws/chat`).
- `src/crypto/`: AES-256-GCM com nonce aleatório de 12 bytes antes do ciphertext; a chave fica em hex em `<config do usuário>/OllaHubBackend/crypto_v2.key`.
- `src/middlewares/`: JWT, CORS com uma única origem, log de requisições e erros no formato `{"error": "..."}`.
- `src/routes/`, `src/controllers/` e `src/repositories/`: CRUD de usuários em `/api/v1`, com repositório em memória.
## Início rápido
Requisitos: Rust (edição 2021) e um Ollama acessível, ou use `POST /v2/api/ollama/install` depois de subir o servidor.
> [!WARNING]
> **No Windows, o servidor mexe na configuração da máquina ao iniciar.** Se estiver elevado (administrador), cria regras de entrada no Firewall para as portas `8080` e `11434` (perfis Domain, Private e Public) e adiciona uma exclusão do Windows Defender para o próprio executável (`Add-MpPreference`). Sem privilégios de administrador, só registra avisos e segue.
```bash
git clone https://git.evandro.dev.br/evandro/OllaHubBackend.git
cd OllaHubBackend
cp .env.example .env # troque JWT_SECRET e ajuste ALLOWED_ORIGINS
cargo run # sobe em http://127.0.0.1:8080
curl http://127.0.0.1:8080/health
curl http://127.0.0.1:8080/v2/api/ollama/check
curl -H "Authorization: Bearer <jwt>" http://127.0.0.1:8080/v2/api/models
```
`cargo build --release` gera o binário em `target/release`; `cargo test` roda o único teste do repositório (ida e volta da criptografia).
## Uso
Swagger UI em `/docs`, OpenAPI em `/api-doc/openapi.json`. Referência completa em [`docs/API.md`](docs/API.md).
| Método | Rota | JWT | O que faz | | Método | Rota | JWT | O que faz |
|---|---|:-:|---| |---|---|:-:|---|
| GET | `/health` | não | Resposta simples `"ok"` | | GET | `/health` | não | Resposta simples `"ok"` |
| GET | `/v2/health` | não | `{"status":"ok"}` | | GET | `/v2/health` | não | `{"status":"ok"}` |
| GET | `/v2/api/ollama/check` | não | Estado do Ollama: no `PATH`, versão, API alcançável (`reachable` / `unreachable` / `degraded`), problemas encontrados | | GET | `/v2/api/ollama/check` | não | Estado do Ollama: `PATH`, versão, API alcançável e problemas encontrados |
| POST | `/v2/api/ollama/install` | não | `202` com `operation_id`; instala em background e espera a API responder (até 60 s) | | POST | `/v2/api/ollama/install` | não | `202` com `operation_id`; instala em background e espera a API responder (até 60 s). Falhas não voltam na resposta |
| GET | `/v2/api/models` | sim | Lista de modelos lida do `modelList.json` | | GET | `/v2/api/models` | sim | Modelos do `modelList.json` |
| POST | `/v2/api/models/install` | sim | `202`; baixa o modelo (`{"name": "..."}`) em background via `/api/pull` do Ollama | | POST | `/v2/api/models/install` | sim | `202`; baixa o modelo (`{"name": "..."}`) em background via `/api/pull` |
| POST | `/v2/api/crypto/set-key` | sim | Define a chave AES-256 (`{"hex_key": "<64 hex>"}`) e a grava em disco | | POST | `/v2/api/crypto/set-key` | sim | Define a chave AES-256 (`{"hex_key": "<64 hex>"}`) e a grava em disco |
| POST | `/v2/api/crypto/rotate-key` | sim | Gera uma chave nova e a devolve em hex | | POST | `/v2/api/crypto/rotate-key` | sim | Gera uma chave nova e a devolve em hex |
| POST | `/v2/api/crypto/encrypt` | sim | `{"data": "texto"}` → `{"ciphertext": "<base64>"}` | | POST | `/v2/api/crypto/encrypt` | sim | `{"data": "texto"}` → `{"ciphertext": "<base64>"}` |
| POST | `/v2/api/crypto/decrypt` | sim | `{"data": "<base64>"}` → `{"plaintext": "texto"}` | | POST | `/v2/api/crypto/decrypt` | sim | `{"data": "<base64>"}` → `{"plaintext": "texto"}` |
| GET/POST | `/api/v1/users` | sim | Lista / cria usuário (nome ≥ 2 caracteres, e-mail válido) | | GET/POST | `/api/v1/users` | sim | Lista ou cria usuário (nome com 2+ caracteres, e-mail válido) |
| GET/PUT/DELETE | `/api/v1/users/{id}` | sim | Lê / atualiza / remove usuário | | GET/PUT/DELETE | `/api/v1/users/{id}` | sim | Lê, atualiza ou remove usuário |
| WS | `/v2/ws` | não | Eco de mensagens e pong; veja [Limitações](#limita%C3%A7%C3%B5es-conhecidas) | | WS | `/v2/ws` | não | Eco de mensagens e pong |
| WS | `/v2/ws/chat` | não | Chat com um modelo do Ollama (abaixo) | | WS | `/v2/ws/chat` | não | Chat com um modelo do Ollama |
Autenticação: `Authorization: Bearer <jwt>`, assinado em HS256 com `JWT_SECRET`, com as claims `sub` e `exp` Autenticação: `Authorization: Bearer <jwt>`, assinado em HS256 com `JWT_SECRET`, com as claims `sub` e `exp` (e `role` opcional). **A API não emite tokens**: o segredo precisa ser o mesmo do serviço que os emite.
(e `role` opcional). **A API não emite tokens** — o segredo precisa ser o mesmo do serviço que os emite (o
`.env.example` indica o frontend).
### Chat por WebSocket (`/v2/ws/chat`) No `/v2/ws/chat`, envie um JSON por mensagem de texto:
Envie um JSON por mensagem de texto:
```json ```json
{ "model": "llama3.2", "messages": [{ "role": "user", "content": "Olá" }], "options": { "temperature": 0.7 } } { "model": "llama3.2", "messages": [{ "role": "user", "content": "Olá" }], "options": { "temperature": 0.7 } }
``` ```
O servidor responde com mensagens de status — `{"status":"checking"}`, `ready`, `installing` (com `progress`), O servidor responde com status (`checking`, `ready`, `installing` com `progress`, `installed`) e depois com as linhas JSON do `/api/chat` do Ollama. Falhas chegam como `{"error": "...", "done": true}`. Há ping a cada 5 s, e a conexão fecha após 30 s sem atividade do cliente.
`installed` — e então as linhas JSON do `/api/chat` do Ollama. **Se o modelo não estiver instalado, ele é baixado
antes de responder.** Falhas chegam como `{"error": "...", "done": true}`. Há ping a cada 5 s e a conexão é
encerrada após 30 s sem atividade do cliente.
## Como funciona
```mermaid
flowchart LR
C["Cliente<br/>OllaHub · curl"] -- "HTTP + JWT" --> S["Actix Web<br/>CORS · logging · JWT"]
C -- WebSocket --> S
S --> V1["/api/v1/users<br/>repositório em memória"]
S --> V2["/v2/api/*<br/>models · ollama · crypto"]
S --> WS["/v2/ws · /v2/ws/chat"]
V2 --> OL["Ollama :11434<br/>/api/tags · /api/pull · /api/chat"]
WS --> OL
V2 --> K["crypto_v2.key<br/>AES-256-GCM"]
S --> D["/docs<br/>Swagger UI"]
```
<details>
<summary>Módulos e detalhes técnicos</summary>
| Módulo | Papel |
|---|---|
| `src/main.rs`, `src/config.rs`, `src/state.rs` | Boot do servidor, configuração por variáveis de ambiente e `AppContext` (broker de eventos, criptografia, URL do Ollama, caminho do `modelList.json`, segredo JWT) |
| `src/routes/`, `src/controllers/`, `src/services/user_service.rs`, `src/repositories/`, `src/models/` | CRUD de usuários (`/api/v1`) com documentação OpenAPI (utoipa) |
| `src/api/` | Rotas v2: saúde, modelos, Ollama e criptografia |
| `src/core/models.rs` | Lê o `modelList.json` (objeto com `models` ou array), verifica modelos instalados (`/api/tags`) e baixa modelos (`/api/pull`) |
| `src/services/ollama_install.rs`, `src/services/install/` | Verificação e instalação do Ollama por sistema: Windows (baixa `OllamaSetup.exe` e roda `/silent`), Linux (`curl -fsSL https://ollama.com/install.sh \| sh`), macOS (baixa o zip e extrai em `/Applications`) |
| `src/crypto/` | AES-256-GCM com nonce aleatório de 12 bytes prefixado ao ciphertext; chave em hex em `<config do usuário>/OllaHubBackend/crypto_v2.key`, carregada na inicialização |
| `src/ws/` | WebSocket de eventos (`/ws`) e de chat (`/ws/chat`), montados sob o escopo `/v2` |
| `src/middlewares/` | JWT, CORS (origem única de `ALLOWED_ORIGINS`), logging de requisições e erros padronizados (`{"error": "..."}`) |
| `src/services/windows_permissions.rs` | Só no Windows: configuração de permissões na inicialização (veja o aviso abaixo) |
| `modelList.json` | Catálogo com 93 modelos (nome, descrição, capacidades, tamanhos, URL na biblioteca do Ollama); a API expõe `name` e `description` |
</details>
> [!WARNING]
> **No Windows, o servidor mexe na configuração da máquina ao iniciar.** Se estiver elevado (administrador), ele
> cria regras de entrada no Firewall para as portas `8080` e `11434` (perfis Domain, Private e Public) e adiciona
> uma exclusão do Windows Defender para o próprio executável (`Add-MpPreference`). Sem privilégios de
> administrador apenas registra os avisos e segue.
## Configuração ## Configuração
Copie `.env.example` para `.env` (carregado com `dotenv` na inicialização). Todas as variáveis são lidas em O `.env` é carregado com `dotenv` na inicialização; as variáveis são lidas em `src/config.rs` e `src/middlewares/`.
`src/config.rs` e `src/middlewares/`:
| Variável | Padrão | Uso | | Variável | Padrão | Uso |
|---|---|---| |---|---|---|
@@ -113,54 +98,28 @@ Copie `.env.example` para `.env` (carregado com `dotenv` na inicialização). To
| `PORT` | `8080` | Porta de escuta | | `PORT` | `8080` | Porta de escuta |
| `OLLAMA_BASE_URL` | `http://localhost:11434` | API do Ollama | | `OLLAMA_BASE_URL` | `http://localhost:11434` | API do Ollama |
| `MODEL_LIST_PATH` | `modelList.json` | Catálogo exposto em `/v2/api/models` | | `MODEL_LIST_PATH` | `modelList.json` | Catálogo exposto em `/v2/api/models` |
| `JWT_SECRET` | `changeme-dev-secret` | Segredo HS256 — **troque**; deve ser o mesmo de quem emite os tokens | | `JWT_SECRET` | `changeme-dev-secret` | Segredo HS256; deve ser o mesmo de quem emite os tokens |
| `ALLOWED_ORIGINS` | `http://localhost:3000` | Única origem aceita pelo CORS (com credenciais) | | `ALLOWED_ORIGINS` | `http://localhost:3000` | Única origem aceita pelo CORS (com credenciais) |
## Executar localmente `SKIP_ANTIVIRUS_WARNINGS` aparece no `.env.example`, mas nenhum código lê essa variável.
Requisitos: toolchain Rust (edição 2021) e um [Ollama](https://ollama.com) acessível — ou use ## Estado do projeto
`POST /v2/api/ollama/install`.
```bash Versão `0.1.0`, com três commits entre 2025-11-05 e 2025-11-18: estrutura inicial com API v2, WebSocket e instalação do Ollama. Limites conhecidos:
git clone https://git.evandro.dev.br/evandro/OllaHubBackend.git
cd OllaHubBackend
cp .env.example .env # ajuste JWT_SECRET e ALLOWED_ORIGINS
cargo run # sobe em http://127.0.0.1:8080
```
```bash - **Usuários só em memória:** o CRUD de `/api/v1/users` perde os dados ao reiniciar.
curl http://127.0.0.1:8080/health - **`/v2/ws` não envia eventos:** só ecoa mensagens e responde a pings. O envio de eventos (`PushEvent`) existe no código, mas nunca é acionado.
curl http://127.0.0.1:8080/v2/api/ollama/check - **Chat sem streaming incremental:** a resposta do Ollama é lida por inteiro (timeout de 300 s) antes de as linhas serem repassadas. O `pull_model` faz o mesmo, então downloads também não mostram progresso em tempo real.
curl -H "Authorization: Bearer <jwt>" http://127.0.0.1:8080/v2/api/models - **Erros silenciosos:** falhas de instalação do Ollama e de download de modelos em background são descartadas.
``` - **Firewall fixo:** a regra criada no Windows usa a porta `8080` literal, mesmo com outro `PORT`.
- **Documentação defasada:** [`docs/OLLAMA.md`](docs/OLLAMA.md) e [`docs/API_EXAMPLES.md`](docs/API_EXAMPLES.md) citam rotas antigas como `/ollama/install-check`; as atuais são `/v2/api/ollama/check` e `/v2/api/ollama/install`.
```bash ## Licença
cargo build --release # binário em target/release
cargo test # o repositório tem um teste: ida e volta da criptografia
```
## Limitações conhecidas [GPL-3.0](LICENSE): você pode usar, estudar, modificar e redistribuir o código, desde que versões distribuídas mantenham o código-fonte aberto sob a mesma licença. Construído com [Actix Web](https://actix.rs), [utoipa](https://github.com/juhaku/utoipa), [jsonwebtoken](https://github.com/Keats/jsonwebtoken), [aes-gcm](https://github.com/RustCrypto/AEADs) e [sysinfo](https://github.com/GuillaumeGomez/sysinfo).
- **Usuários só em memória:** o CRUD de `/api/v1/users` usa um repositório em memória; os dados somem ao <div align="center">
reiniciar.
- **`/v2/ws` não envia eventos:** o endpoint apenas ecoa mensagens e responde a pings. O envio de eventos ao
cliente (`PushEvent`) existe no código mas nunca é acionado, e o progresso de instalações vai para outro
broker.
- **Chat sem streaming incremental:** o corpo da resposta do Ollama é lido por inteiro (timeout de 300 s) antes
de as linhas serem repassadas, então os chunks chegam juntos ao fim da geração.
- **Downloads sem progresso em tempo real:** `pull_model` também lê a resposta inteira e só depois publica as
linhas e o evento final.
- **Instalação do Ollama:** roda instaladores baixados da internet no sistema operacional atual
(Windows/Linux/macOS); sistemas não suportados retornam erro.
- **Documentação defasada em um ponto:** [`docs/OLLAMA.md`](docs/OLLAMA.md) cita rotas legadas
(`/ollama/install-check`, `/ollama/install`) — as rotas atuais são `/v2/api/ollama/check` e
`/v2/api/ollama/install`.
- **`SKIP_ANTIVIRUS_WARNINGS`:** consta no `.env.example`, mas nenhum código lê essa variável.
- **Firewall fixo na porta 8080:** a regra criada no Windows usa `8080` literal, mesmo com `PORT` diferente.
## Avisos <sub>Feito por <a href="https://evandro.dev.br">Evandro Fonseca Junior</a> · <a href="https://git.evandro.dev.br/evandro">git.evandro.dev.br</a></sub>
- Licença: GPL-3.0 (veja [`LICENSE`](LICENSE)). </div>
- Construído com [Actix Web](https://actix.rs), [utoipa](https://github.com/juhaku/utoipa) (Swagger UI),
[jsonwebtoken](https://github.com/Keats/jsonwebtoken), [aes-gcm](https://github.com/RustCrypto/AEADs) e
[sysinfo](https://github.com/GuillaumeGomez/sysinfo).
+44
View File
@@ -0,0 +1,44 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="420" viewBox="0 0 1200 420" role="img" aria-labelledby="title desc">
<title id="title">OllaHubBackend</title>
<desc id="desc">OllaHubBackend: tabela de rotas do servidor na porta 8080, com a verificação e a instalação do Ollama, a lista de modelos protegida por JWT e o chat por WebSocket, todas ligadas ao Ollama na porta 11434.</desc>
<rect width="1200" height="420" rx="24" fill="#18181B"/>
<g id="title-block" font-family="system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif">
<text x="64" y="170" font-size="64" font-weight="700" fill="#FAFAFA">OllaHubBackend</text>
<text x="64" y="226" font-size="28" fill="#FAFAFA">API HTTP e WebSocket em Rust</text>
<text x="64" y="264" font-size="28" fill="#FAFAFA">para instalar e operar o Ollama.</text>
<text x="64" y="322" font-size="20" fill="#A1A1AA" font-family="ui-monospace, 'SF Mono', Consolas, monospace">Rust · Actix Web 4.11 · utoipa · JWT</text>
</g>
<g id="routes" font-family="ui-monospace, 'SF Mono', Consolas, monospace" font-size="20">
<rect x="632" y="56" width="504" height="236" rx="14" fill="#09090B" stroke="#3F3F46" stroke-width="2"/>
<text x="656" y="88" fill="#FAFAFA">:8080</text>
<text x="1112" y="88" font-size="18" fill="#A1A1AA" text-anchor="end" font-family="system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif">Swagger em /docs</text>
<path d="M632 106 h504" stroke="#27272A" stroke-width="2"/>
<text x="656" y="142" fill="#A1A1AA">GET</text>
<text x="728" y="142" fill="#FAFAFA">/v2/api/ollama/check</text>
<text x="656" y="182" fill="#A1A1AA">POST</text>
<text x="728" y="182" fill="#FAFAFA">/v2/api/ollama/install</text>
<text x="1112" y="182" fill="#00BC7D" text-anchor="end">202</text>
<text x="656" y="222" fill="#A1A1AA">GET</text>
<text x="728" y="222" fill="#FAFAFA">/v2/api/models</text>
<g fill="none" stroke="#FE9A00" stroke-width="2.5" stroke-linejoin="round">
<rect x="1050" y="206" width="18" height="16" rx="3" fill="#FE9A00"/>
<path d="M1053 206 v-5 a6 6 0 0 1 12 0 v5"/>
</g>
<text x="1112" y="222" font-size="18" fill="#FE9A00" text-anchor="end">JWT</text>
<text x="656" y="262" fill="#A1A1AA">WS</text>
<text x="728" y="262" fill="#FAFAFA">/v2/ws/chat</text>
</g>
<g id="ollama" font-family="ui-monospace, 'SF Mono', Consolas, monospace">
<path d="M884 294 v26 M876 312 l8 8 l8 -8" fill="none" stroke="#A1A1AA" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="774" y="326" width="220" height="44" rx="10" fill="#09090B" stroke="#3F3F46" stroke-width="2"/>
<text x="884" y="355" font-size="20" fill="#FAFAFA" text-anchor="middle">Ollama :11434</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB