diff --git a/docs/AI.md b/docs/AI.md index 89a2c6f..9e6ef14 100644 --- a/docs/AI.md +++ b/docs/AI.md @@ -38,6 +38,7 @@ Valores são persistidos em `config/ai.yaml`. | Superfície | Detalhe | |------------|---------| | Portal → **Análise → Insights IA** (e preview na Visão geral) | Resumo, lista de insights e gráficos sugeridos | +| Portal → **Navegador** → Grafana (`ciem-insights`) | Mesma pasta de insights no Grafana embutido | | Portal → Visão Geral | Linha de status dos insights | | Grafana → dashboard `ciem-insights` | Tabelas via Infinity (`/grafana/insights/table` e `/charts`) | | Grafana → Visão Geral NOC | Painel “Insights de IA” | diff --git a/docs/CHANGELOG_FEATURES.md b/docs/CHANGELOG_FEATURES.md index 94f29bf..afec488 100644 --- a/docs/CHANGELOG_FEATURES.md +++ b/docs/CHANGELOG_FEATURES.md @@ -20,7 +20,7 @@ Resumo das capacidades adicionadas ao CIEM para operação pelo portal (administ | Switch ON/OFF | Admin ativa/desativa cada coletor em **Configuração** | | Formulário de opções | Com o módulo ativo, aparecem URL, credenciais e opções específicas | | Persistência | Valores gravados em `config/modules.yaml` | -| Grafana embutido | Aba Grafana no portal com visão NOC via API | +| Consoles no Navegador | Com o módulo ativo, `options.url` aparece como atalho no **Navegador HTML5** (admin) | **Documentação:** [MODULES.md](MODULES.md), [PORTAL.md](PORTAL.md) diff --git a/docs/CI_CD.md b/docs/CI_CD.md index 09f500d..0e8833c 100644 --- a/docs/CI_CD.md +++ b/docs/CI_CD.md @@ -31,6 +31,15 @@ CONFIG_PATH=./config PYTHONPATH=shared:services/core pytest tests -v docker compose -f deploy/docker/docker-compose.yml config --quiet ``` +Testes do portal cobrem o **Navegador HTML5** (`tests/test_portal_browser.py`: sidebar para todos, chrome, normalização de URL, assets/docs) e lembretes/calendário (`tests/test_portal_reminders_calendar.py`). + +Regenerar diagrama e mockups (incl. tela do Navegador): + +```bash +python3 scripts/generate_architecture_diagram.py +python3 scripts/generate_portal_mockups.py +``` + ## Releases versionadas ```bash diff --git a/docs/DASHBOARDS.md b/docs/DASHBOARDS.md index 93218fb..ef6808c 100644 --- a/docs/DASHBOARDS.md +++ b/docs/DASHBOARDS.md @@ -2,6 +2,8 @@ O CIEM provisiona **13 dashboards** na pasta **CIEM** do Grafana (`grafana/dashboards/`). +No dia a dia do NOC, abra-os pelo **Navegador HTML5** do portal (sidebar **Navegador** → atalho Grafana), sem sair do CIEM. Detalhes: [GRAFANA.md](GRAFANA.md) · [PORTAL.md](PORTAL.md). + ## Visão geral dos dashboards ### Plataforma (6) diff --git a/docs/GRAFANA.md b/docs/GRAFANA.md index 35fcb58..5a8adf4 100644 --- a/docs/GRAFANA.md +++ b/docs/GRAFANA.md @@ -2,6 +2,19 @@ O CIEM provisiona automaticamente **13 dashboards** (6 da plataforma incluindo Insights IA + 6 por módulo + visão de módulos). +## Acesso recomendado: Navegador HTML5 do portal + +Preferência operacional (observer e admin): + +1. Login no portal → sidebar **Navegador** +2. Atalho **Grafana** (ou digite `/grafana/` na barra) +3. Dashboards embutidos no iframe same-origin; use **↗** se precisar de tela cheia em nova aba + +Atalhos equivalentes: Visão geral → **Abrir navegador**; Análise → **No navegador**. +URL direta: `https://seu-dominio/grafana/` (via proxy ZTNA). + +![Navegador com Grafana](assets/ciem-portal-browser.jpg) + ## Dashboards por módulo Cada sistema de monitoramento tem um dashboard dedicado: @@ -43,7 +56,7 @@ python3 scripts/generate-grafana-dashboards.py | **Módulos Coletores** | `ciem-modules` | Saúde de Zabbix, Cacti, Nagios, etc. | | **Sessões e Auditoria** | `ciem-sessions` | Alvos de manutenção + log de sessões | -Acesse: `https://seu-dominio/grafana/` → pasta **CIEM** +Acesse pelo **Navegador** do portal ou em `https://seu-dominio/grafana/` → pasta **CIEM** ## Fontes de dados provisionadas diff --git a/docs/MANUAL_ADMIN.md b/docs/MANUAL_ADMIN.md index 0ae78bf..0617a48 100644 --- a/docs/MANUAL_ADMIN.md +++ b/docs/MANUAL_ADMIN.md @@ -76,6 +76,8 @@ Além do Grafana e URLs, o admin usa o **Navegador** para: ![Navegador HTML5](assets/ciem-portal-browser.jpg) +O mockup mostra o item **Navegador** na sidebar (sem restrição de papel), chrome completo e atalhos Grafana / Guacamole / módulos. + ## Sessões de manutenção ![Sessões](assets/ciem-portal-sessions.jpg) diff --git a/docs/MANUAL_USER.md b/docs/MANUAL_USER.md index 20eac2a..dadf697 100644 --- a/docs/MANUAL_USER.md +++ b/docs/MANUAL_USER.md @@ -92,7 +92,7 @@ Se Insights estiverem desabilitados, a aba informa que um administrador precisa ![Navegador HTML5](assets/ciem-portal-browser.jpg) -Disponível para **todos** os papéis desde o login: +Disponível para **todos** os papéis desde o login (mockup acima: sidebar com **Navegador** ativo, chrome e Grafana embutido): - Sidebar **Navegador** (barra de endereço, voltar/avançar, recarregar, início) - Atalhos: Grafana (`/grafana/`), URLs recentes diff --git a/docs/PORTAL.md b/docs/PORTAL.md index c66f5f6..485650a 100644 --- a/docs/PORTAL.md +++ b/docs/PORTAL.md @@ -142,6 +142,7 @@ services/portal/public/ 1. **Branch** — `cursor/portal-` ou fluxo do time 2. **Mockup** — se a UI mudar de forma visível, atualize o JPG em `docs/assets/` + (`python3 scripts/generate_portal_mockups.py` regenera login, dashboard, **navegador**, alarmes, análise, sessões e config; `python3 scripts/generate_architecture_diagram.py` regenera o diagrama) 3. **PR** — inclua screenshot ou diff do mockup 4. **Teste manual** — login com `admin` / `admin123` diff --git a/docs/PROCESSES.md b/docs/PROCESSES.md index 54b9494..b756f7b 100644 --- a/docs/PROCESSES.md +++ b/docs/PROCESSES.md @@ -6,6 +6,8 @@ Descrição dos processos de ponta a ponta: coleta, alarmes, visualização e ma ![Arquitetura e fluxos](assets/ciem-architecture-diagram.jpg) +O diagrama destaca o **Portal Web + Navegador HTML5** (Grafana / URLs / SSO Guacamole) atrás do proxy ZTNA. Mockup da tela: [ciem-portal-browser.jpg](assets/ciem-portal-browser.jpg). + ## 1. Autenticação no portal ``` diff --git a/docs/README.md b/docs/README.md index 43f25c0..688b828 100644 --- a/docs/README.md +++ b/docs/README.md @@ -67,6 +67,8 @@ Imagens em `docs/assets/` para alinhar times de produto, UX e desenvolvimento: | [ciem-architecture-diagram.jpg](assets/ciem-architecture-diagram.jpg) | Arquitetura ZTNA (Portal + Navegador HTML5) | > Ao alterar o portal (`services/portal/`), atualize o mockup correspondente (incl. `ciem-portal-browser.jpg`) e referencie o PR na descrição da mudança. +> +> Regenerar: `python3 scripts/generate_portal_mockups.py` · `python3 scripts/generate_architecture_diagram.py` ## Manifests Kubernetes diff --git a/docs/assets/ciem-config-interface.jpg b/docs/assets/ciem-config-interface.jpg new file mode 100644 index 0000000..7bbd3a9 Binary files /dev/null and b/docs/assets/ciem-config-interface.jpg differ diff --git a/docs/assets/ciem-config-interface.png b/docs/assets/ciem-config-interface.png index d5fd1d6..0714437 100644 Binary files a/docs/assets/ciem-config-interface.png and b/docs/assets/ciem-config-interface.png differ diff --git a/docs/assets/ciem-portal-alarms.jpg b/docs/assets/ciem-portal-alarms.jpg index 20d6b6a..66e214e 100644 Binary files a/docs/assets/ciem-portal-alarms.jpg and b/docs/assets/ciem-portal-alarms.jpg differ diff --git a/docs/assets/ciem-portal-alarms.png b/docs/assets/ciem-portal-alarms.png new file mode 100644 index 0000000..3b37734 Binary files /dev/null and b/docs/assets/ciem-portal-alarms.png differ diff --git a/docs/assets/ciem-portal-analysis.jpg b/docs/assets/ciem-portal-analysis.jpg index 7ff2cb8..320ecb0 100644 Binary files a/docs/assets/ciem-portal-analysis.jpg and b/docs/assets/ciem-portal-analysis.jpg differ diff --git a/docs/assets/ciem-portal-analysis.png b/docs/assets/ciem-portal-analysis.png index e5e21c4..6d2a539 100644 Binary files a/docs/assets/ciem-portal-analysis.png and b/docs/assets/ciem-portal-analysis.png differ diff --git a/docs/assets/ciem-portal-browser.jpg b/docs/assets/ciem-portal-browser.jpg index 153bc87..ff24877 100644 Binary files a/docs/assets/ciem-portal-browser.jpg and b/docs/assets/ciem-portal-browser.jpg differ diff --git a/docs/assets/ciem-portal-browser.png b/docs/assets/ciem-portal-browser.png index 338a877..e46365e 100644 Binary files a/docs/assets/ciem-portal-browser.png and b/docs/assets/ciem-portal-browser.png differ diff --git a/docs/assets/ciem-portal-dashboard.jpg b/docs/assets/ciem-portal-dashboard.jpg index 678dbf6..a52077c 100644 Binary files a/docs/assets/ciem-portal-dashboard.jpg and b/docs/assets/ciem-portal-dashboard.jpg differ diff --git a/docs/assets/ciem-portal-dashboard.png b/docs/assets/ciem-portal-dashboard.png index ed5d7a3..4a9486f 100644 Binary files a/docs/assets/ciem-portal-dashboard.png and b/docs/assets/ciem-portal-dashboard.png differ diff --git a/docs/assets/ciem-portal-login.jpg b/docs/assets/ciem-portal-login.jpg index fca486b..28501ba 100644 Binary files a/docs/assets/ciem-portal-login.jpg and b/docs/assets/ciem-portal-login.jpg differ diff --git a/docs/assets/ciem-portal-login.png b/docs/assets/ciem-portal-login.png new file mode 100644 index 0000000..c998f48 Binary files /dev/null and b/docs/assets/ciem-portal-login.png differ diff --git a/docs/assets/ciem-portal-sessions.jpg b/docs/assets/ciem-portal-sessions.jpg index 201ffb0..2cdeb70 100644 Binary files a/docs/assets/ciem-portal-sessions.jpg and b/docs/assets/ciem-portal-sessions.jpg differ diff --git a/docs/assets/ciem-portal-sessions.png b/docs/assets/ciem-portal-sessions.png index dde8093..87e42d3 100644 Binary files a/docs/assets/ciem-portal-sessions.png and b/docs/assets/ciem-portal-sessions.png differ diff --git a/scripts/generate_portal_mockups.py b/scripts/generate_portal_mockups.py new file mode 100644 index 0000000..18b78ba --- /dev/null +++ b/scripts/generate_portal_mockups.py @@ -0,0 +1,366 @@ +#!/usr/bin/env python3 +"""Gera mockups JPG/PNG do portal destacando o Navegador HTML5 na sidebar.""" +from __future__ import annotations + +import os +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + +ROOT = Path(__file__).resolve().parents[1] +OUT = ROOT / "docs" / "assets" + +W, H = 1280, 800 +BG = "#0c1118" +SIDE = "#0a1016" +SURF = "#16202c" +ELEV = "#121a24" +LINE = "#314559" +TEXT = "#e8eef6" +SOFT = "#9aabbd" +TEAL = "#2ec4b6" +TEAL_DIM = "#0f2a2a" +BLUE = "#6cb6ff" +ORANGE = "#e6a23c" +RED = "#f07178" +GREEN = "#7dcea0" + + +def font(size: int, bold: bool = False) -> ImageFont.ImageFont: + name = "DejaVuSans-Bold.ttf" if bold else "DejaVuSans.ttf" + path = f"/usr/share/fonts/truetype/dejavu/{name}" + if os.path.exists(path): + return ImageFont.truetype(path, size) + return ImageFont.load_default() + + +def rr(d: ImageDraw.ImageDraw, xy, fill, outline=LINE, width=1, radius=10): + d.rounded_rectangle(xy, radius=radius, fill=fill, outline=outline, width=width) + + +def save(img: Image.Image, stem: str) -> None: + OUT.mkdir(parents=True, exist_ok=True) + png = OUT / f"{stem}.png" + jpg = OUT / f"{stem}.jpg" + img.save(png, "PNG", optimize=True) + img.convert("RGB").save(jpg, "JPEG", quality=90, optimize=True) + print(f"Wrote {png.name} and {jpg.name}") + + +def draw_shell(d: ImageDraw.ImageDraw, active: str) -> None: + d.rectangle((0, 0, 220, H), fill=SIDE) + d.rectangle((220, 0, W, H), fill=BG) + d.line((220, 0, 220, H), fill=LINE, width=1) + ft, fb, fs = font(18, True), font(13, True), font(12) + d.text((24, 28), "CIEM", fill=TEAL, font=ft) + d.text((88, 34), "NOC", fill=SOFT, font=fs) + + items = [ + ("dashboard", "Visão geral"), + ("browser", "Navegador"), + ("alarms", "Alarmes"), + ("history", "Histórico"), + ("analysis", "Análise"), + ("sessions", "Sessões"), + ("config", "Configuração"), + ] + y = 90 + for key, label in items: + selected = key == active + if selected: + rr(d, (12, y - 8, 208, y + 28), fill=TEAL_DIM, outline=TEAL, width=1, radius=8) + d.text((28, y), label, fill=TEAL, font=fb) + else: + d.text((28, y), label, fill=SOFT, font=fs) + y += 44 + + d.text((24, H - 70), "admin (admin)", fill=SOFT, font=fs) + rr(d, (24, H - 48, 196, H - 18), fill=ELEV, outline=LINE, width=1, radius=8) + d.text((78, H - 40), "Sair", fill=SOFT, font=fs) + + +def mock_login() -> None: + img = Image.new("RGB", (W, H), BG) + d = ImageDraw.Draw(img) + ft, fh, fs = font(42, True), font(20, True), font(14) + # brand plane + d.rectangle((0, 0, int(W * 0.55), H), fill="#0a1418") + d.ellipse((-80, -40, 420, 360), outline=(46, 196, 182, 40), width=2) + d.text((72, 220), "CIEM", fill=TEAL, font=ft) + d.text((72, 290), "Centro Integrado de", fill=TEXT, font=fh) + d.text((72, 322), "Estatística e Manutenção", fill=TEXT, font=fh) + d.text( + (72, 380), + "NOC unificado: alarmes, Navegador HTML5,", + fill=SOFT, + font=fs, + ) + d.text((72, 404), "coletores, sessões auditadas e insights.", fill=SOFT, font=fs) + # form + rr(d, (780, 180, 1180, 620), fill=SURF, outline=LINE, width=1, radius=14) + d.text((820, 220), "Entrar", fill=TEXT, font=fh) + d.text((820, 280), "Usuário", fill=SOFT, font=fs) + rr(d, (820, 305, 1140, 345), fill=ELEV, outline=LINE, width=1, radius=8) + d.text((835, 315), "admin", fill=TEXT, font=fs) + d.text((820, 370), "Senha", fill=SOFT, font=fs) + rr(d, (820, 395, 1140, 435), fill=ELEV, outline=LINE, width=1, radius=8) + d.text((835, 405), "••••••••", fill=TEXT, font=fs) + rr(d, (820, 480, 1140, 530), fill=TEAL, outline=TEAL, width=1, radius=8) + d.text((900, 494), "Acessar portal", fill="#06201c", font=font(15, True)) + save(img, "ciem-portal-login") + + +def mock_dashboard() -> None: + img = Image.new("RGB", (W, H), BG) + d = ImageDraw.Draw(img) + draw_shell(d, "dashboard") + ft, fh, fs = font(22, True), font(14, True), font(12) + d.text((248, 28), "Visão geral", fill=TEXT, font=ft) + d.text((248, 60), "Estado operacional · atalho para o Navegador", fill=SOFT, font=fs) + rr(d, (980, 28, 1240, 68), fill="#2a1518", outline=RED, width=1, radius=20) + d.text((1000, 40), "3 alarmes Abrir", fill=RED, font=fs) + + # KPIs + kpis = [("Críticos", "1", RED), ("Warnings", "2", ORANGE), ("Total", "3", BLUE), ("Módulos", "4/5", GREEN)] + x = 248 + for title, val, color in kpis: + rr(d, (x, 100, x + 220, 190), fill=SURF, outline=LINE, width=1, radius=12) + d.text((x + 16, 118), title, fill=SOFT, font=fs) + d.text((x + 16, 148), val, fill=color, font=ft) + x += 240 + + rr(d, (248, 220, 820, 520), fill=SURF, outline=LINE, width=1, radius=12) + d.text((268, 240), "Distribuição de severidade", fill=TEXT, font=fh) + bars = [(RED, 90), (ORANGE, 140), (BLUE, 60), (GREEN, 40)] + bx = 300 + for color, h in bars: + d.rectangle((bx, 480 - h, bx + 70, 480), fill=color) + bx += 110 + + rr(d, (840, 220, 1240, 520), fill=SURF, outline=LINE, width=1, radius=12) + d.text((860, 240), "Insights", fill=TEXT, font=fh) + d.text((860, 270), "Atualizado · heurístico", fill=SOFT, font=fs) + rr(d, (860, 310, 1220, 390), fill=ELEV, outline=LINE, width=1, radius=8) + d.text((875, 330), "Pico de warning em Zabbix", fill=TEXT, font=fs) + d.text((875, 355), "Revisar triggers de interface", fill=SOFT, font=fs) + rr(d, (860, 430, 1120, 475), fill=ELEV, outline=TEAL, width=1, radius=8) + d.text((880, 444), "Abrir análise completa", fill=TEAL, font=fs) + + rr(d, (248, 540, 1240, 760), fill=SURF, outline=LINE, width=1, radius=12) + d.text((268, 560), "Coletores", fill=TEXT, font=fh) + d.text((268, 585), "Saúde dos módulos · use o Navegador para consoles", fill=SOFT, font=fs) + rr(d, (1000, 555, 1210, 595), fill=TEAL_DIM, outline=TEAL, width=1, radius=8) + d.text((1030, 567), "Abrir navegador", fill=TEAL, font=font(13, True)) + mods = [("zabbix", GREEN), ("cacti", GREEN), ("nagios", ORANGE), ("topdesk", GREEN)] + mx = 268 + for name, color in mods: + rr(d, (mx, 630, mx + 200, 720), fill=ELEV, outline=LINE, width=1, radius=8) + d.text((mx + 16, 655), name, fill=TEXT, font=fh) + d.text((mx + 16, 685), "● Online" if color == GREEN else "● Atenção", fill=color, font=fs) + mx += 220 + + # reminder chip + rr(d, (980, 700, 1230, 750), fill="#142018", outline=TEAL, width=1, radius=10) + d.text((1000, 716), "Lembretes · 2 abertos", fill=TEAL, font=fs) + save(img, "ciem-portal-dashboard") + + +def mock_browser() -> None: + img = Image.new("RGB", (W, H), BG) + d = ImageDraw.Draw(img) + draw_shell(d, "browser") + ft, fh, fs = font(22, True), font(14, True), font(12) + d.text((248, 24), "Navegador", fill=TEXT, font=ft) + d.text((248, 54), "Grafana, consoles e URLs embutidos no portal", fill=SOFT, font=fs) + + # chrome + rr(d, (248, 90, 1240, 150), fill=SURF, outline=LINE, width=1, radius=10) + for i, sym in enumerate(("←", "→", "↻", "⌂")): + x0 = 260 + i * 42 + rr(d, (x0, 104, x0 + 34, 136), fill=ELEV, outline=LINE, width=1, radius=6) + d.text((x0 + 10, 112), sym, fill=TEXT, font=fs) + rr(d, (440, 104, 1120, 136), fill=ELEV, outline=TEAL, width=1, radius=6) + d.text((455, 112), "/grafana/", fill=TEXT, font=font(13)) + rr(d, (1130, 104, 1185, 136), fill=TEAL, outline=TEAL, width=1, radius=6) + d.text((1145, 112), "Ir", fill="#06201c", font=font(13, True)) + rr(d, (1195, 104, 1230, 136), fill=ELEV, outline=LINE, width=1, radius=6) + d.text((1204, 112), "↗", fill=TEAL, font=fs) + + # presets + chips = ["Início", "Grafana", "Guacamole", "zabbix", "cacti"] + cx = 248 + for i, label in enumerate(chips): + active = label == "Grafana" + rr( + d, + (cx, 162, cx + 110, 192), + fill=TEAL_DIM if active else ELEV, + outline=TEAL if active else LINE, + width=1, + radius=8, + ) + d.text((cx + 18, 170), label, fill=TEAL if active else SOFT, font=fs) + cx += 120 + + # stage / iframe content + rr(d, (248, 210, 1240, 760), fill="#0b1214", outline=LINE, width=1, radius=12) + d.rectangle((248, 210, 1240, 258), fill="#111a22") + d.text((268, 224), "Grafana · Visão Geral NOC", fill=SOFT, font=fs) + # fake panels + panels = [ + (268, 280, 580, 480, "Alarmes ativos", "12"), + (600, 280, 920, 480, "Severidade", "crit 2 · warn 5"), + (940, 280, 1220, 480, "Módulos", "5 online"), + (268, 500, 740, 730, "Tendência 24h", ""), + (760, 500, 1220, 730, "Top hosts", ""), + ] + for x0, y0, x1, y1, title, val in panels: + rr(d, (x0, y0, x1, y1), fill=SURF, outline=LINE, width=1, radius=8) + d.text((x0 + 16, y0 + 16), title, fill=SOFT, font=fs) + if val: + d.text((x0 + 16, y0 + 70), val, fill=TEXT, font=ft) + else: + d.line((x0 + 30, y1 - 40, x1 - 30, y0 + 90), fill=TEAL, width=2) + save(img, "ciem-portal-browser") + + +def mock_alarms() -> None: + img = Image.new("RGB", (W, H), BG) + d = ImageDraw.Draw(img) + draw_shell(d, "alarms") + ft, fh, fs = font(22, True), font(14, True), font(12) + d.text((248, 28), "Alarmes", fill=TEXT, font=ft) + d.text((248, 58), "Priorize critical e high", fill=SOFT, font=fs) + rr(d, (248, 100, 1240, 740), fill=SURF, outline=LINE, width=1, radius=12) + d.text((268, 120), "Alarmes ativos", fill=TEXT, font=fh) + rows = [ + (RED, "CRITICAL", "Interface eth0 down — core-sw-01", "zabbix"), + (ORANGE, "HIGH", "CPU > 90% — app-02", "zabbix"), + (ORANGE, "WARNING", "Disco 85% — db-01", "cacti"), + (BLUE, "INFO", "Backup concluído", "topdesk"), + ] + y = 170 + for color, sev, msg, mod in rows: + rr(d, (268, y, 1220, y + 70), fill=ELEV, outline=LINE, width=1, radius=8) + d.text((288, y + 16), f"[{sev}]", fill=color, font=font(12, True)) + d.text((400, y + 16), msg, fill=TEXT, font=fs) + d.text((400, y + 40), mod, fill=SOFT, font=fs) + y += 90 + save(img, "ciem-portal-alarms") + + +def mock_analysis() -> None: + img = Image.new("RGB", (W, H), BG) + d = ImageDraw.Draw(img) + draw_shell(d, "analysis") + ft, fh, fs = font(22, True), font(13, True), font(12) + d.text((248, 24), "Análise", fill=TEXT, font=ft) + d.text((248, 54), "Gráficos, insights e detalhe filtrado", fill=SOFT, font=fs) + tabs = ["Resumo", "Insights IA", "Alarmes", "Módulos", "Histórico"] + x = 248 + for i, t in enumerate(tabs): + active = i == 0 + rr( + d, + (x, 90, x + 110, 122), + fill=TEAL_DIM if active else SURF, + outline=TEAL if active else LINE, + width=1, + radius=8, + ) + d.text((x + 18, 98), t, fill=TEAL if active else SOFT, font=fs) + x += 118 + rr(d, (980, 90, 1110, 122), fill=TEAL_DIM, outline=TEAL, width=1, radius=8) + d.text((995, 98), "No navegador", fill=TEAL, font=fs) + rr(d, (1120, 90, 1240, 122), fill=ELEV, outline=LINE, width=1, radius=8) + d.text((1140, 98), "Grafana ↗", fill=SOFT, font=fs) + + rr(d, (248, 150, 820, 740), fill=SURF, outline=LINE, width=1, radius=12) + d.text((268, 170), "Gráfico de análise", fill=TEXT, font=fh) + d.rectangle((300, 520, 360, 680), fill=RED) + d.rectangle((400, 480, 460, 680), fill=ORANGE) + d.rectangle((500, 560, 560, 680), fill=BLUE) + d.rectangle((600, 600, 660, 680), fill=GREEN) + + rr(d, (840, 150, 1240, 740), fill=SURF, outline=LINE, width=1, radius=12) + d.text((860, 170), "Detalhe", fill=TEXT, font=fh) + for i, line in enumerate( + ("Resumo consolidado do turno", "IA: 2 recomendações", "4 módulos online", "Histórico recente OK") + ): + y = 220 + i * 70 + rr(d, (860, y, 1220, y + 55), fill=ELEV, outline=LINE, width=1, radius=8) + d.text((880, y + 18), line, fill=TEXT, font=fs) + save(img, "ciem-portal-analysis") + + +def mock_sessions() -> None: + img = Image.new("RGB", (W, H), BG) + d = ImageDraw.Draw(img) + draw_shell(d, "sessions") + ft, fh, fs = font(22, True), font(14, True), font(12) + d.text((248, 28), "Sessões", fill=TEXT, font=ft) + d.text((248, 58), "SSO Guacamole — no navegador ou nova aba", fill=SOFT, font=fs) + rr(d, (248, 100, 1240, 740), fill=SURF, outline=LINE, width=1, radius=12) + d.text((268, 120), "Sessões de manutenção", fill=TEXT, font=fh) + rr(d, (880, 115, 1040, 150), fill=TEAL_DIM, outline=TEAL, width=1, radius=8) + d.text((900, 125), "No navegador", fill=TEAL, font=fs) + rr(d, (1050, 115, 1220, 150), fill=TEAL, outline=TEAL, width=1, radius=8) + d.text((1065, 125), "Guacamole ↗", fill="#06201c", font=font(12, True)) + d.text((268, 180), "Alvos", fill=SOFT, font=fs) + for i, (name, proto) in enumerate( + (("core-sw-01", "SSH"), ("fw-edge", "SSH"), ("jump-win", "RDP")) + ): + y = 210 + i * 90 + rr(d, (268, y, 1220, y + 75), fill=ELEV, outline=LINE, width=1, radius=8) + d.text((288, y + 16), name, fill=TEXT, font=fh) + d.text((288, y + 42), f"{proto} · manutenção", fill=SOFT, font=fs) + rr(d, (900, y + 20, 1040, y + 55), fill=TEAL_DIM, outline=TEAL, width=1, radius=8) + d.text((920, y + 30), "No navegador", fill=TEAL, font=fs) + rr(d, (1055, y + 20, 1200, y + 55), fill=SURF, outline=LINE, width=1, radius=8) + d.text((1075, y + 30), "Nova aba ↗", fill=SOFT, font=fs) + save(img, "ciem-portal-sessions") + + +def mock_config() -> None: + img = Image.new("RGB", (W, H), BG) + d = ImageDraw.Draw(img) + draw_shell(d, "config") + ft, fh, fs = font(22, True), font(14, True), font(12) + d.text((248, 28), "Configuração", fill=TEXT, font=ft) + d.text((248, 58), "Usuários · LDAP · IA · Módulos", fill=SOFT, font=fs) + sections = ["Usuários", "LDAP", "IA", "Módulos"] + y = 110 + for i, s in enumerate(sections): + active = i == 0 + rr( + d, + (248, y, 430, y + 44), + fill=TEAL_DIM if active else SURF, + outline=TEAL if active else LINE, + width=1, + radius=8, + ) + d.text((270, y + 12), s, fill=TEAL if active else SOFT, font=fs) + y += 56 + rr(d, (450, 110, 1240, 740), fill=SURF, outline=LINE, width=1, radius=12) + d.text((480, 140), "Usuários locais", fill=TEXT, font=fh) + d.text((480, 170), "Independentes do LDAP. Admin padrão: admin", fill=SOFT, font=fs) + for i, (u, role) in enumerate((("admin", "admin"), ("observador", "observer"))): + y = 220 + i * 80 + rr(d, (480, y, 1200, y + 65), fill=ELEV, outline=LINE, width=1, radius=8) + d.text((500, y + 22), f"{u} · {role}", fill=TEXT, font=fs) + save(img, "ciem-config-interface") + + +def main() -> None: + mock_login() + mock_dashboard() + mock_browser() + mock_alarms() + mock_analysis() + mock_sessions() + mock_config() + + +if __name__ == "__main__": + main() diff --git a/tests/test_portal_browser.py b/tests/test_portal_browser.py index bb8a8e6..c246ad1 100644 --- a/tests/test_portal_browser.py +++ b/tests/test_portal_browser.py @@ -163,3 +163,13 @@ def test_docs_and_assets_mention_browser(): assert "ciem-portal-browser" in portal_md assert (ROOT / "docs" / "assets" / "ciem-portal-browser.jpg").is_file() assert (ROOT / "docs" / "assets" / "ciem-architecture-diagram.png").is_file() + assert (ROOT / "scripts" / "generate_portal_mockups.py").is_file() + assert (ROOT / "scripts" / "generate_architecture_diagram.py").is_file() + grafana_md = (ROOT / "docs" / "GRAFANA.md").read_text(encoding="utf-8") + assert "Navegador" in grafana_md + processes_md = (ROOT / "docs" / "PROCESSES.md").read_text(encoding="utf-8") + assert "ciem-portal-browser.jpg" in processes_md + manual_user = (ROOT / "docs" / "MANUAL_USER.md").read_text(encoding="utf-8") + assert "Navegador HTML5" in manual_user + manual_admin = (ROOT / "docs" / "MANUAL_ADMIN.md").read_text(encoding="utf-8") + assert "Navegador" in manual_admin