chore(desk): processo staging modern UI — topnav + servicos merge (sem prod)
Adiciona index.staging.html, topnav.js, desk-modern.css, app.staging.js, snapshot VM122, script deploy-staging-only e checklist antes de cutover prod. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c1881f58e6
commit
a1e4f43dbb
11 changed files with 16435 additions and 0 deletions
49
deploy/desk-staging-modern/deploy-staging-only.sh
Executable file
49
deploy/desk-staging-modern/deploy-staging-only.sh
Executable file
|
|
@ -0,0 +1,49 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Desk modern UI — deploy APENAS staging (desk-staging.ligbox.com.br)
|
||||||
|
# NÃO toca em ligbox-ops-platform_frontend_1 (produção desk.ligbox.com.br)
|
||||||
|
#
|
||||||
|
# Uso: ./deploy-staging-only.sh [VM122_HOST]
|
||||||
|
# Spec / processo: docs/anais-referencia/20260626_DESK_STAGING_MODERN_PROCESS.md
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HOST="${1:-root@10.10.10.122}"
|
||||||
|
PASS="${DESK_SSH_PASS:-805353}"
|
||||||
|
CONTAINER="ligbox-ops-platform-staging_frontend-staging_1"
|
||||||
|
ROOT="$(cd "$(dirname "$0")/../../projects/ops-desk/frontend" && pwd)"
|
||||||
|
HTML="${ROOT}/assets"
|
||||||
|
|
||||||
|
echo "==> Desk staging modern — fonte: ${ROOT}"
|
||||||
|
echo "==> Container: ${CONTAINER} (NÃO produção)"
|
||||||
|
|
||||||
|
sshpass -p "${PASS}" scp -o StrictHostKeyChecking=no \
|
||||||
|
"${ROOT}/index.staging.html" \
|
||||||
|
"${HTML}/topnav.js" \
|
||||||
|
"${HTML}/desk-modern.css" \
|
||||||
|
"${HTML}/app.staging.js" \
|
||||||
|
"${HTML}/styles.css" \
|
||||||
|
"${HTML}/servicos.js" \
|
||||||
|
"${HTML}/auth.js" \
|
||||||
|
"${HOST}:/tmp/desk-staging-deploy/"
|
||||||
|
|
||||||
|
sshpass -p "${PASS}" ssh -o StrictHostKeyChecking=no "${HOST}" bash -s <<EOF
|
||||||
|
set -e
|
||||||
|
C=${CONTAINER}
|
||||||
|
docker cp /tmp/desk-staging-deploy/index.staging.html \$C:/usr/share/nginx/html/index.html
|
||||||
|
docker cp /tmp/desk-staging-deploy/topnav.js \$C:/usr/share/nginx/html/assets/topnav.js
|
||||||
|
docker cp /tmp/desk-staging-deploy/desk-modern.css \$C:/usr/share/nginx/html/assets/desk-modern.css
|
||||||
|
docker cp /tmp/desk-staging-deploy/app.staging.js \$C:/usr/share/nginx/html/assets/app.staging.js
|
||||||
|
docker cp /tmp/desk-staging-deploy/styles.css \$C:/usr/share/nginx/html/assets/styles.css
|
||||||
|
docker cp /tmp/desk-staging-deploy/servicos.js \$C:/usr/share/nginx/html/assets/servicos.js
|
||||||
|
docker cp /tmp/desk-staging-deploy/auth.js \$C:/usr/share/nginx/html/assets/auth.js
|
||||||
|
echo "=== Verificação staging ==="
|
||||||
|
docker exec \$C grep "20260626staging1" /usr/share/nginx/html/index.html | head -3
|
||||||
|
docker exec \$C grep -c "navigateScope\\|PURGE_BLOCKLIST" /usr/share/nginx/html/assets/servicos.js
|
||||||
|
docker exec \$C test -f /usr/share/nginx/html/assets/desk-modern.css && echo "desk-modern.css OK"
|
||||||
|
docker exec \$C test -f /usr/share/nginx/html/assets/app.staging.js && echo "app.staging.js OK"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Deploy staging concluído."
|
||||||
|
echo "Testar: https://desk-staging.ligbox.com.br/?desk=1 (Ctrl+Shift+R)"
|
||||||
|
echo "Produção desk.ligbox.com.br NÃO foi alterada."
|
||||||
116
docs/anais-referencia/20260626_DESK_STAGING_MODERN_PROCESS.md
Normal file
116
docs/anais-referencia/20260626_DESK_STAGING_MODERN_PROCESS.md
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
# Desk Modern UI — Processo staging (Roger)
|
||||||
|
|
||||||
|
**Data:** 2026-06-26
|
||||||
|
**URL staging:** https://desk-staging.ligbox.com.br/?desk=1
|
||||||
|
**URL produção:** https://desk.ligbox.com.br — **NÃO promover até checklist OK**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Unificar no **staging**:
|
||||||
|
|
||||||
|
1. **Design novo** — topnav por zonas (Operações · Comercial · Infra · Sistema)
|
||||||
|
2. **Funcionalidades prod** — Serviços IaaS (Escopo OPS cards, blocklist, loading modal)
|
||||||
|
3. **Repo Git** — fonte única antes de cutover produção
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ficheiros no repo
|
||||||
|
|
||||||
|
| Ficheiro | Papel |
|
||||||
|
|----------|--------|
|
||||||
|
| `frontend/index.staging.html` | HTML moderno (shell v2) — **só staging** |
|
||||||
|
| `frontend/index.html` | Produção — **inalterado** até cutover |
|
||||||
|
| `frontend/assets/topnav.js` | Nav zonas + context sidebar |
|
||||||
|
| `frontend/assets/desk-modern.css` | CSS chrome v0.12 (extraído staging) |
|
||||||
|
| `frontend/assets/app.staging.js` | app.js prod + hooks topnav + SSO/cutover |
|
||||||
|
| `frontend/assets/servicos.js` | Prod (Escopo OPS, blocklist) |
|
||||||
|
| `frontend/staging-snapshot/` | Snapshot VM122 25/06 (referência) |
|
||||||
|
| `deploy/desk-staging-modern/deploy-staging-only.sh` | Deploy **só** container staging |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deploy staging (comando)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x deploy/desk-staging-modern/deploy-staging-only.sh
|
||||||
|
./deploy/desk-staging-modern/deploy-staging-only.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Container alvo: `ligbox-ops-platform-staging_frontend-staging_1`
|
||||||
|
**Nunca** usar `ligbox-ops-platform_frontend_1` neste script.
|
||||||
|
|
||||||
|
Cache bust staging: `?v=20260626staging1`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checklist antes de produção
|
||||||
|
|
||||||
|
Roger valida em **desk-staging** — só depois promover.
|
||||||
|
|
||||||
|
### Layout / nav
|
||||||
|
|
||||||
|
- [ ] Header glass + zonas visíveis
|
||||||
|
- [ ] Tab activa correcta ao mudar view
|
||||||
|
- [ ] Link **Console** abre console.ligbox.com.br
|
||||||
|
- [ ] Context sidebar (Tickets / Eventos / Matriz) funciona
|
||||||
|
- [ ] Mobile / zoom 125% sem quebra grave
|
||||||
|
|
||||||
|
### Serviços IaaS
|
||||||
|
|
||||||
|
- [ ] Lista clientes alinhada (badge 🔒 blocklist)
|
||||||
|
- [ ] Escopo OPS — cards clicáveis + Spec
|
||||||
|
- [ ] Modal purge — loading animado + purge blocklist
|
||||||
|
- [ ] Purge domínio teste (não blocklist) — job completa
|
||||||
|
|
||||||
|
### Integrações
|
||||||
|
|
||||||
|
- [ ] Login / logout
|
||||||
|
- [ ] Console handoff (`openConsolePath`)
|
||||||
|
- [ ] Agentic Ops carrega sem flash infinito
|
||||||
|
- [ ] API staging responde (`/api/health`)
|
||||||
|
|
||||||
|
### Erros conhecidos corrigidos neste merge
|
||||||
|
|
||||||
|
- [x] `consumeSsoFromUrl` / `consoleHomeUrl` ausentes no app staging antigo
|
||||||
|
- [x] `servicos.js` staging desactualizado (19/06) → usa prod
|
||||||
|
|
||||||
|
### Produção — bloqueado até
|
||||||
|
|
||||||
|
- [ ] Roger assina checklist acima
|
||||||
|
- [ ] Commit + tag `desk-modern-v0.12`
|
||||||
|
- [ ] Deploy script separado `promote-staging-to-prod.sh` (criar depois)
|
||||||
|
- [ ] Janela rollback documentada
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Diferença staging vs prod (actual)
|
||||||
|
|
||||||
|
| | Staging | Produção |
|
||||||
|
|---|---------|----------|
|
||||||
|
| URL | desk-staging.ligbox.com.br | desk.ligbox.com.br |
|
||||||
|
| Layout | shell v2 + topnav | sidebar legado |
|
||||||
|
| servicos.js | merge prod (20260626staging1) | align1 |
|
||||||
|
| app | app.staging.js | app.js |
|
||||||
|
| API | api-staging container | api produção |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Specs relacionadas
|
||||||
|
|
||||||
|
- **019** — Console v2 / cutover Fase 4
|
||||||
|
- **018** — Serviços IaaS / Escopo OPS
|
||||||
|
- **030** — Mission Board (referência visual)
|
||||||
|
- Checkpoint 2026-06-21 — shell partilhada console.ligbox.com.br
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollback staging
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Restaurar index anterior no container staging (se backup existir)
|
||||||
|
ssh root@10.10.10.122 'docker exec ligbox-ops-platform-staging_frontend-staging_1 ls /usr/share/nginx/html/index.html*'
|
||||||
|
```
|
||||||
|
|
||||||
|
Ou rebuild: `docker-compose -f docker-compose.agentic-staging.yml build frontend-staging`
|
||||||
4744
projects/ops-desk/frontend/assets/app.staging.js
Normal file
4744
projects/ops-desk/frontend/assets/app.staging.js
Normal file
File diff suppressed because it is too large
Load diff
421
projects/ops-desk/frontend/assets/desk-modern.css
Normal file
421
projects/ops-desk/frontend/assets/desk-modern.css
Normal file
|
|
@ -0,0 +1,421 @@
|
||||||
|
/* ── Desk chrome v0.12 — glass header, tabs rectas (sem pills) ── */
|
||||||
|
.shell--v2 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
grid-template-columns: unset;
|
||||||
|
background: var(--bg);
|
||||||
|
font-family: 'Inter', 'DM Sans', system-ui, -apple-system, sans-serif;
|
||||||
|
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell--v2 .sidebar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-chrome--modern {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 200;
|
||||||
|
background: rgba(255, 253, 249, 0.78);
|
||||||
|
backdrop-filter: blur(16px) saturate(1.35);
|
||||||
|
-webkit-backdrop-filter: blur(16px) saturate(1.35);
|
||||||
|
border-bottom: 1px solid rgba(92, 46, 46, 0.08);
|
||||||
|
box-shadow:
|
||||||
|
0 1px 0 rgba(255, 255, 255, 0.65) inset,
|
||||||
|
0 8px 32px rgba(92, 46, 46, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-chrome__inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
min-height: 58px;
|
||||||
|
padding: 0.45rem 1.25rem;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-chrome__brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.55rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-chrome__mark {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
border-radius: 0;
|
||||||
|
background: linear-gradient(135deg, #ff5c8a 0%, #5c2e2e 100%);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-chrome__brand h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 650;
|
||||||
|
letter-spacing: -0.035em;
|
||||||
|
color: #2b2622;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-chrome__brand .brand-ops {
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-chrome__nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0.15rem 0.25rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
mask-image: linear-gradient(90deg, transparent, #000 12px, #000 calc(100% - 12px), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-chrome__nav::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-zone {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.22rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0.2rem 0.35rem 0.15rem;
|
||||||
|
border-radius: 0;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-zone.active {
|
||||||
|
background: rgba(92, 46, 46, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-zone__label {
|
||||||
|
font-size: 0.56rem;
|
||||||
|
font-weight: 650;
|
||||||
|
letter-spacing: 0.11em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #9a928a;
|
||||||
|
line-height: 1;
|
||||||
|
padding-left: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-zone__links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background: rgba(92, 46, 46, 0.05);
|
||||||
|
border: 1px solid rgba(92, 46, 46, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
appearance: none;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #5f5852;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 520;
|
||||||
|
padding: 0.42rem 0.75rem;
|
||||||
|
border-radius: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
border-right: 1px solid rgba(92, 46, 46, 0.08);
|
||||||
|
transition:
|
||||||
|
color 0.16s ease,
|
||||||
|
background 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-zone__links .nav-link:last-child {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
color: #2b2622;
|
||||||
|
background: rgba(255, 255, 255, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active {
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 650;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: inset 0 -2px 0 var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-zone--console {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
align-self: center;
|
||||||
|
padding: 0.15rem 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-zone--console .nav-zone__label {
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link--console {
|
||||||
|
display: inline-block;
|
||||||
|
color: #0f4c81;
|
||||||
|
font-weight: 620;
|
||||||
|
border: 1px solid rgba(14, 165, 233, 0.28);
|
||||||
|
background: #f0f9ff;
|
||||||
|
margin-top: 0.12rem;
|
||||||
|
border-radius: 0;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link--console:hover {
|
||||||
|
color: #0c4a6e;
|
||||||
|
background: #e0f2fe;
|
||||||
|
box-shadow: none;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-zone__sep {
|
||||||
|
width: 1px;
|
||||||
|
align-self: center;
|
||||||
|
height: 28px;
|
||||||
|
margin: 0 0.1rem;
|
||||||
|
background: linear-gradient(180deg, transparent, rgba(92, 46, 46, 0.12), transparent);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-chrome__meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-left: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-chrome__meta .header-user {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.55rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-right: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-user__avatar {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
border-radius: 0;
|
||||||
|
background: linear-gradient(145deg, #5c2e2e, #8b3a42);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 650;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-user__text strong {
|
||||||
|
display: block;
|
||||||
|
color: #2b2622;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 620;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-user__text > span {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: #9a928a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-chrome__meta .status-pill {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
padding: 0.28rem 0.55rem;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-chrome__meta .btn-ghost {
|
||||||
|
border-radius: 0;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
font-weight: 550;
|
||||||
|
border-color: rgba(92, 46, 46, 0.1);
|
||||||
|
color: #5f5852;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-chrome__meta .btn-ghost:hover {
|
||||||
|
background: rgba(92, 46, 46, 0.06);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 0 1fr;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell--v2:not(.shell--no-context) .workspace {
|
||||||
|
grid-template-columns: 232px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-main {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell--v2 .main {
|
||||||
|
padding: 1.1rem 1.5rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-sidebar {
|
||||||
|
background: rgba(255, 253, 249, 0.92);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border-right: 1px solid rgba(92, 46, 46, 0.08);
|
||||||
|
padding: 1rem 0.75rem 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: calc(100vh - 58px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-sidebar__head {
|
||||||
|
padding: 0 0.35rem 0.75rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-sidebar__label {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.62rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-sidebar__title {
|
||||||
|
margin: 0.2rem 0 0;
|
||||||
|
font-size: 1.02rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #3a3530;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-nav {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-nav .toolbar,
|
||||||
|
.context-nav .team-admin-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
|
align-items: stretch;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-nav .toolbar-sep {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-nav .filter-btn,
|
||||||
|
.context-nav .team-admin-tab {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
justify-content: flex-start;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0.48rem 0.65rem;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-nav .filter-btn.active,
|
||||||
|
.context-nav .team-admin-tab.active {
|
||||||
|
background: var(--accent-soft);
|
||||||
|
border-color: rgba(92, 46, 46, 0.25);
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-nav .am-role-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-nav .am-role-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-nav .am-role-btn {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-sidebar__foot {
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell--v2 .page-header {
|
||||||
|
margin-bottom: 0.85rem;
|
||||||
|
padding-bottom: 0.65rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell--v2 .page-header h2 {
|
||||||
|
font-size: 1.35rem;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell--v2 .page-toolbar:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell--v2 #access-matrix-content .am-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.app-chrome__inner {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 0.55rem 0.75rem;
|
||||||
|
gap: 0.65rem;
|
||||||
|
}
|
||||||
|
.app-chrome__nav {
|
||||||
|
order: 3;
|
||||||
|
flex-basis: 100%;
|
||||||
|
mask-image: none;
|
||||||
|
}
|
||||||
|
.app-chrome__meta .header-user__text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.shell--v2:not(.shell--no-context) .workspace {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.context-sidebar {
|
||||||
|
min-height: auto;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
}
|
||||||
128
projects/ops-desk/frontend/assets/topnav.js
Normal file
128
projects/ops-desk/frontend/assets/topnav.js
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
/** Desk chrome nav — tabs planas por zona + sidebar contextual (v0.11). */
|
||||||
|
|
||||||
|
const TOPNAV_VIEW_GROUP = {
|
||||||
|
'agentic-ops': 'operacoes',
|
||||||
|
dashboard: 'operacoes',
|
||||||
|
tickets: 'operacoes',
|
||||||
|
events: 'operacoes',
|
||||||
|
'overview-home': 'comercial',
|
||||||
|
leads: 'comercial',
|
||||||
|
infra: 'infra',
|
||||||
|
infra2: 'infra',
|
||||||
|
'email-migration': 'infra',
|
||||||
|
overview: 'sistema',
|
||||||
|
account: 'sistema',
|
||||||
|
messages: 'sistema',
|
||||||
|
admin: 'sistema',
|
||||||
|
'access-matrix': 'sistema',
|
||||||
|
modules: 'sistema',
|
||||||
|
};
|
||||||
|
|
||||||
|
const CONTEXT_TOOLBARS = {
|
||||||
|
tickets: { title: 'Tickets', host: '#view-tickets', label: 'Filas' },
|
||||||
|
events: { title: 'Eventos', host: '#view-events', label: 'Vistas' },
|
||||||
|
};
|
||||||
|
|
||||||
|
function initTopnavDropdowns() {
|
||||||
|
/* v0.11 — tabs planas; sem dropdowns */
|
||||||
|
}
|
||||||
|
|
||||||
|
function parkContextToolbars() {
|
||||||
|
Object.values(CONTEXT_TOOLBARS).forEach(({ host }) => {
|
||||||
|
const view = document.querySelector(host);
|
||||||
|
const toolbar = view?.querySelector('.toolbar');
|
||||||
|
if (toolbar && view && toolbar.parentElement !== view) {
|
||||||
|
view.insertBefore(toolbar, view.firstChild);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const matrixRoleNav = document.getElementById('context-matrix-roles');
|
||||||
|
if (matrixRoleNav && matrixRoleNav.dataset.parkedFrom) {
|
||||||
|
const slot = document.querySelector(matrixRoleNav.dataset.parkedFrom);
|
||||||
|
if (slot) slot.appendChild(matrixRoleNav);
|
||||||
|
delete matrixRoleNav.dataset.parkedFrom;
|
||||||
|
}
|
||||||
|
const adminHost = document.getElementById('context-admin-tabs');
|
||||||
|
if (adminHost) adminHost.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTopnavActive(view) {
|
||||||
|
document.querySelectorAll('[data-desk-nav]').forEach((btn) => {
|
||||||
|
const active = btn.dataset.view === view;
|
||||||
|
btn.classList.toggle('active', active);
|
||||||
|
if (active) btn.setAttribute('aria-current', 'page');
|
||||||
|
else btn.removeAttribute('aria-current');
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.nav-zone[data-topnav-group]').forEach((zone) => {
|
||||||
|
zone.classList.toggle('active', TOPNAV_VIEW_GROUP[view] === zone.dataset.topnavGroup);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountMatrixRoleSidebar() {
|
||||||
|
const roleNav = document.querySelector('#access-matrix-content .am-role-list');
|
||||||
|
if (!roleNav) return false;
|
||||||
|
const nav = document.getElementById('context-nav');
|
||||||
|
if (!nav) return false;
|
||||||
|
roleNav.dataset.parkedFrom = '#access-matrix-content .am-layout';
|
||||||
|
nav.appendChild(roleNav);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateContextSidebar(view) {
|
||||||
|
const shell = document.querySelector('.shell');
|
||||||
|
const sidebar = document.getElementById('context-sidebar');
|
||||||
|
const nav = document.getElementById('context-nav');
|
||||||
|
const titleEl = document.getElementById('context-sidebar-title');
|
||||||
|
const labelEl = document.getElementById('context-sidebar-label');
|
||||||
|
if (!shell || !sidebar || !nav) return;
|
||||||
|
|
||||||
|
parkContextToolbars();
|
||||||
|
nav.innerHTML = '';
|
||||||
|
|
||||||
|
if (view === 'admin' && typeof renderAdminTabs === 'function') {
|
||||||
|
sidebar.hidden = false;
|
||||||
|
shell.classList.remove('shell--no-context');
|
||||||
|
if (titleEl) titleEl.textContent = 'Administradores';
|
||||||
|
if (labelEl) labelEl.textContent = 'Secções';
|
||||||
|
nav.innerHTML = `<div id="context-admin-tabs">${renderAdminTabs()}</div>`;
|
||||||
|
if (typeof bindAdminTabs === 'function') bindAdminTabs();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view === 'access-matrix') {
|
||||||
|
sidebar.hidden = false;
|
||||||
|
shell.classList.remove('shell--no-context');
|
||||||
|
if (titleEl) titleEl.textContent = 'Matriz de Acesso';
|
||||||
|
if (labelEl) labelEl.textContent = 'Funções';
|
||||||
|
requestAnimationFrame(() => mountMatrixRoleSidebar());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cfg = CONTEXT_TOOLBARS[view];
|
||||||
|
if (!cfg) {
|
||||||
|
sidebar.hidden = true;
|
||||||
|
shell.classList.add('shell--no-context');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewEl = document.querySelector(cfg.host);
|
||||||
|
const toolbar = viewEl?.querySelector('.toolbar');
|
||||||
|
if (!toolbar) {
|
||||||
|
sidebar.hidden = true;
|
||||||
|
shell.classList.add('shell--no-context');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sidebar.hidden = false;
|
||||||
|
shell.classList.remove('shell--no-context');
|
||||||
|
if (titleEl) titleEl.textContent = cfg.title;
|
||||||
|
if (labelEl) labelEl.textContent = cfg.label;
|
||||||
|
toolbar.classList.add('context-toolbar');
|
||||||
|
nav.appendChild(toolbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.DeskTopnav = {
|
||||||
|
initTopnavDropdowns,
|
||||||
|
updateTopnavActive,
|
||||||
|
updateContextSidebar,
|
||||||
|
remountMatrixRoles: mountMatrixRoleSidebar,
|
||||||
|
};
|
||||||
438
projects/ops-desk/frontend/index.staging.html
Normal file
438
projects/ops-desk/frontend/index.staging.html
Normal file
|
|
@ -0,0 +1,438 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
<title>Ligbox Ops — Support Desk</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400..700&display=swap" rel="stylesheet"/>
|
||||||
|
<link rel="stylesheet" href="/assets/styles.css?v=20260626staging1"/>
|
||||||
|
<link rel="stylesheet" href="/assets/desk-modern.css?v=20260626staging1"/>
|
||||||
|
<link rel="stylesheet" href="/assets/agentic-ops.css?v=20260623iconagentes"/>
|
||||||
|
<link rel="stylesheet" href="/assets/access-matrix.css?v=20260623matriz"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<svg width="0" height="0" style="position:absolute;visibility:hidden" aria-hidden="true" focusable="false">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="lg-dashboard" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#f6f6f8"/>
|
||||||
|
<stop offset="100%" stop-color="#d8d6dc"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="lg-overview" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#ff9fb5"/>
|
||||||
|
<stop offset="100%" stop-color="#8d233d"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="lg-ticket" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#ffd36d"/>
|
||||||
|
<stop offset="100%" stop-color="#e29b20"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="lg-target" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#ff9aa4"/>
|
||||||
|
<stop offset="100%" stop-color="#d11f42"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="lg-calendar" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#b38dff"/>
|
||||||
|
<stop offset="100%" stop-color="#6b42c9"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="lg-building" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#b9dbff"/>
|
||||||
|
<stop offset="100%" stop-color="#4c8fd5"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="lg-server" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#8b929e"/>
|
||||||
|
<stop offset="100%" stop-color="#4b4f58"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="lg-gear" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#f2f3f7"/>
|
||||||
|
<stop offset="100%" stop-color="#9aa0ab"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="lg-chat" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#6fc1ff"/>
|
||||||
|
<stop offset="100%" stop-color="#2f6ef0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="lg-admin" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#c07aff"/>
|
||||||
|
<stop offset="100%" stop-color="#6c2eb9"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<filter id="nav-shadow" x="-25%" y="-25%" width="150%" height="150%">
|
||||||
|
<feDropShadow dx="0" dy="3" stdDeviation="2.5" flood-color="#000000" flood-opacity="0.28"/>
|
||||||
|
</filter>
|
||||||
|
|
||||||
|
<symbol id="icon-dashboard" viewBox="0 0 64 64">
|
||||||
|
<rect x="8" y="6" width="48" height="52" rx="10" fill="url(#lg-dashboard)" stroke="#b8b3bf" stroke-width="2"/>
|
||||||
|
<rect x="14" y="12" width="36" height="40" rx="6" fill="#f7fbff" stroke="#c9d2db"/>
|
||||||
|
<rect x="18" y="32" width="8" height="16" rx="2" fill="#2dc7b7"/>
|
||||||
|
<rect x="29" y="22" width="8" height="26" rx="2" fill="#57b5ff"/>
|
||||||
|
<rect x="40" y="16" width="8" height="32" rx="2" fill="#ef6179"/>
|
||||||
|
<rect x="12" y="8" width="44" height="6" rx="3" fill="#ffffff" opacity="0.55"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="icon-overview" viewBox="0 0 64 64">
|
||||||
|
<path d="M32 6 51 13v15c0 14-8.9 23.7-19 29-10.1-5.3-19-15-19-29V13L32 6Z" fill="url(#lg-overview)" stroke="#f1c2cf" stroke-width="2.2"/>
|
||||||
|
<path d="M32 13 45 18v10c0 9.5-5.8 16.5-13 20.8-7.2-4.3-13-11.3-13-20.8V18L32 13Z" fill="#fff3f6" opacity="0.85"/>
|
||||||
|
<path d="M20 18h24v10c0 8.6-5.2 14.9-12 18.9-6.8-4-12-10.3-12-18.9V18Z" fill="#8e1f3a"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="icon-tickets" viewBox="0 0 64 64">
|
||||||
|
<path d="M10 18a6 6 0 0 0 6-6h32a6 6 0 0 0 6 6v8a6 6 0 0 0 0 12v8a6 6 0 0 0-6 6H16a6 6 0 0 0-6-6v-8a6 6 0 0 0 0-12v-8Z" fill="url(#lg-ticket)" stroke="#d38b1a" stroke-width="2"/>
|
||||||
|
<path d="M24 18v28" stroke="#f9e0a9" stroke-width="2" stroke-dasharray="3 3"/>
|
||||||
|
<path d="m39 22 2.8 5.6 6.2.9-4.5 4.4 1.1 6.2-5.6-2.9-5.6 2.9 1.1-6.2-4.5-4.4 6.2-.9L39 22Z" fill="#7d4510"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="icon-leads" viewBox="0 0 64 64">
|
||||||
|
<circle cx="30" cy="34" r="20" fill="url(#lg-target)" stroke="#f1a5b1" stroke-width="2"/>
|
||||||
|
<circle cx="30" cy="34" r="13" fill="#ffd9df" opacity="0.88"/>
|
||||||
|
<circle cx="30" cy="34" r="7" fill="#fff"/>
|
||||||
|
<circle cx="30" cy="34" r="3.8" fill="#df2d51"/>
|
||||||
|
<path d="m37 27 13-9 4 4-9 13-1.6-6.5L37 27Z" fill="#4ba3ff" stroke="#2d6fd1" stroke-width="1.4" stroke-linejoin="round"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="icon-events" viewBox="0 0 64 64">
|
||||||
|
<rect x="10" y="14" width="44" height="38" rx="9" fill="url(#lg-calendar)" stroke="#cdb8ff" stroke-width="2"/>
|
||||||
|
<rect x="10" y="14" width="44" height="10" rx="9" fill="#f7efff" opacity="0.9"/>
|
||||||
|
<path d="M20 10v10M44 10v10" stroke="#7c52dd" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<g fill="#efe7ff">
|
||||||
|
<rect x="18" y="28" width="6" height="6" rx="1.2"/>
|
||||||
|
<rect x="28" y="28" width="6" height="6" rx="1.2"/>
|
||||||
|
<rect x="38" y="28" width="6" height="6" rx="1.2"/>
|
||||||
|
<rect x="18" y="38" width="6" height="6" rx="1.2"/>
|
||||||
|
<rect x="28" y="38" width="6" height="6" rx="1.2"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="47" cy="46" r="9" fill="#8a57da" stroke="#e6d8ff" stroke-width="1.8"/>
|
||||||
|
<path d="m47 40 1.7 3.5 3.8.6-2.7 2.6.6 3.8-3.4-1.8-3.4 1.8.6-3.8-2.7-2.6 3.8-.6L47 40Z" fill="#fff3a7"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="icon-tenants" viewBox="0 0 64 64">
|
||||||
|
<rect x="14" y="10" width="28" height="42" rx="4" fill="url(#lg-building)" stroke="#7ab3ea" stroke-width="2"/>
|
||||||
|
<rect x="28" y="18" width="22" height="34" rx="4" fill="#98cbf7" stroke="#5f9ed8" stroke-width="2"/>
|
||||||
|
<g fill="#eaf6ff">
|
||||||
|
<rect x="19" y="16" width="5" height="5" rx="1"/>
|
||||||
|
<rect x="27" y="16" width="5" height="5" rx="1"/>
|
||||||
|
<rect x="19" y="25" width="5" height="5" rx="1"/>
|
||||||
|
<rect x="27" y="25" width="5" height="5" rx="1"/>
|
||||||
|
<rect x="19" y="34" width="5" height="5" rx="1"/>
|
||||||
|
<rect x="27" y="34" width="5" height="5" rx="1"/>
|
||||||
|
<rect x="33" y="23" width="5" height="5" rx="1"/>
|
||||||
|
<rect x="41" y="23" width="5" height="5" rx="1"/>
|
||||||
|
<rect x="33" y="32" width="5" height="5" rx="1"/>
|
||||||
|
<rect x="41" y="32" width="5" height="5" rx="1"/>
|
||||||
|
</g>
|
||||||
|
<rect x="29" y="42" width="8" height="10" rx="1.5" fill="#5a7ca0"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="icon-infra" viewBox="0 0 64 64">
|
||||||
|
<rect x="16" y="8" width="32" height="48" rx="6" fill="url(#lg-server)" stroke="#969daa" stroke-width="2"/>
|
||||||
|
<g fill="#2a2f38" stroke="#6c727d" stroke-width="1">
|
||||||
|
<rect x="20" y="14" width="24" height="10" rx="2"/>
|
||||||
|
<rect x="20" y="28" width="24" height="10" rx="2"/>
|
||||||
|
<rect x="20" y="42" width="24" height="8" rx="2"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<circle cx="25" cy="19" r="1.8" fill="#ff5f72"/>
|
||||||
|
<circle cx="25" cy="33" r="1.8" fill="#ff5f72"/>
|
||||||
|
<circle cx="25" cy="46" r="1.6" fill="#ff5f72"/>
|
||||||
|
<rect x="30" y="18" width="10" height="2" rx="1" fill="#d9dde5"/>
|
||||||
|
<rect x="30" y="32" width="10" height="2" rx="1" fill="#d9dde5"/>
|
||||||
|
<rect x="30" y="45" width="10" height="2" rx="1" fill="#d9dde5"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="icon-account" viewBox="0 0 64 64">
|
||||||
|
<circle cx="32" cy="32" r="10" fill="#872338"/>
|
||||||
|
<path d="M32 10 36.5 14.1 42.7 13.3 45 19.1 50.9 21.4 50.1 27.6 54.2 32 50.1 36.4 50.9 42.6 45 44.9 42.7 50.7 36.5 49.9 32 54 27.5 49.9 21.3 50.7 19 44.9 13.1 42.6 13.9 36.4 9.8 32 13.9 27.6 13.1 21.4 19 19.1 21.3 13.3 27.5 14.1Z" fill="url(#lg-gear)" stroke="#b9bec8" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
<circle cx="32" cy="32" r="11.5" fill="none" stroke="#dfe2e8" stroke-width="2"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="icon-messages" viewBox="0 0 64 64">
|
||||||
|
<path d="M15 14h26c6.1 0 11 4.9 11 11v9c0 6.1-4.9 11-11 11H31l-10 8v-8h-6c-6.1 0-11-4.9-11-11v-9c0-6.1 4.9-11 11-11Z" fill="url(#lg-chat)" stroke="#7fccff" stroke-width="2"/>
|
||||||
|
<rect x="16" y="24" width="20" height="3.4" rx="1.7" fill="#eef7ff"/>
|
||||||
|
<rect x="16" y="31" width="24" height="3.4" rx="1.7" fill="#eef7ff"/>
|
||||||
|
<rect x="16" y="38" width="17" height="3.4" rx="1.7" fill="#eef7ff"/>
|
||||||
|
<circle cx="48" cy="18" r="7" fill="#ff6d85" stroke="#ffb1be" stroke-width="2"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="icon-admin" viewBox="0 0 64 64">
|
||||||
|
<circle cx="24" cy="25" r="11" fill="url(#lg-admin)" stroke="#d8b7ff" stroke-width="2"/>
|
||||||
|
<circle cx="41" cy="26" r="10" fill="#8a45dd" stroke="#d8b7ff" stroke-width="2"/>
|
||||||
|
<path d="M10 51c0-8.3 6.7-15 15-15h1c8.3 0 15 6.7 15 15v3H10v-3Z" fill="#7a34cb"/>
|
||||||
|
<path d="M30 52c0-7.2 5.8-13 13-13s13 5.8 13 13v2H30v-2Z" fill="#5c27a3"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="icon-overview-home" viewBox="0 0 64 64">
|
||||||
|
<rect x="8" y="10" width="48" height="44" rx="8" fill="#eef4ff" stroke="#9bbcff" stroke-width="2"/>
|
||||||
|
<rect x="14" y="16" width="14" height="12" rx="3" fill="#2f6fed" opacity="0.9"/>
|
||||||
|
<rect x="32" y="16" width="18" height="12" rx="3" fill="#dbeafe"/>
|
||||||
|
<rect x="14" y="32" width="36" height="16" rx="3" fill="#fff" stroke="#c7d7fe"/>
|
||||||
|
<path d="M14 32h36" stroke="#c7d7fe"/>
|
||||||
|
<rect x="18" y="36" width="10" height="2.5" rx="1" fill="#64748b"/>
|
||||||
|
<rect x="32" y="36" width="14" height="2.5" rx="1" fill="#64748b"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="icon-infra2" viewBox="0 0 64 64">
|
||||||
|
<rect x="6" y="8" width="52" height="48" rx="8" fill="#0f172a" stroke="#38bdf8" stroke-width="2"/>
|
||||||
|
<circle cx="32" cy="32" r="14" fill="none" stroke="#22d3ee" stroke-width="2" opacity="0.5"/>
|
||||||
|
<circle cx="32" cy="32" r="8" fill="none" stroke="#38bdf8" stroke-width="1.5" opacity="0.7"/>
|
||||||
|
<path d="M32 18v28M18 32h28" stroke="#38bdf8" stroke-width="1" opacity="0.35"/>
|
||||||
|
<circle cx="32" cy="32" r="3" fill="#22d3ee"/>
|
||||||
|
<circle cx="44" cy="22" r="4" fill="#ef4444" stroke="#fecaca" stroke-width="1.2"/>
|
||||||
|
<path d="M36 28 L41 24" stroke="#f87171" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<rect x="12" y="12" width="8" height="3" rx="1" fill="#22c55e" opacity="0.9"/>
|
||||||
|
<rect x="12" y="49" width="14" height="3" rx="1" fill="#64748b"/>
|
||||||
|
</symbol>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
<div class="shell shell--v2 shell--no-context">
|
||||||
|
<header class="app-chrome app-chrome--modern">
|
||||||
|
<div class="app-chrome__inner">
|
||||||
|
<div class="app-chrome__brand">
|
||||||
|
<span class="app-chrome__mark" aria-hidden="true"></span>
|
||||||
|
<h1>Ligbox <span class="brand-ops">Ops</span></h1>
|
||||||
|
</div>
|
||||||
|
<nav class="app-chrome__nav" aria-label="Módulos Desk">
|
||||||
|
<div class="nav-zone" data-topnav-group="operacoes">
|
||||||
|
<span class="nav-zone__label">Operações</span>
|
||||||
|
<div class="nav-zone__links">
|
||||||
|
<button type="button" data-view="agentic-ops" data-module="agentic-ops" id="nav-agentic-ops" data-desk-nav class="nav-link">Agentic Ops</button>
|
||||||
|
<button type="button" data-view="dashboard" data-module="core" data-desk-nav class="nav-link">Dashboard</button>
|
||||||
|
<button type="button" data-view="tickets" data-module="core" data-desk-nav class="nav-link">Tickets</button>
|
||||||
|
<button type="button" data-view="events" data-module="events" id="nav-events" data-desk-nav class="nav-link">Eventos</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-zone__sep" aria-hidden="true"></div>
|
||||||
|
<div class="nav-zone" data-topnav-group="comercial">
|
||||||
|
<span class="nav-zone__label">Comercial</span>
|
||||||
|
<div class="nav-zone__links">
|
||||||
|
<button type="button" data-view="overview-home" data-module="overview-home" id="nav-overview-home" data-desk-nav class="nav-link">Serviços</button>
|
||||||
|
<button type="button" data-view="leads" data-module="leads" id="nav-leads" hidden data-desk-nav class="nav-link">Leads</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-zone__sep" aria-hidden="true"></div>
|
||||||
|
<a id="nav-console-return" class="nav-zone nav-zone--console" href="https://console.ligbox.com.br">
|
||||||
|
<span class="nav-zone__label">Plataforma</span>
|
||||||
|
<span class="nav-link nav-link--console">Console</span>
|
||||||
|
</a>
|
||||||
|
<div class="nav-zone__sep" aria-hidden="true"></div>
|
||||||
|
<div class="nav-zone" data-topnav-group="infra">
|
||||||
|
<span class="nav-zone__label">Infra</span>
|
||||||
|
<div class="nav-zone__links">
|
||||||
|
<button type="button" data-view="infra" data-module="infra" data-desk-nav class="nav-link">Infra</button>
|
||||||
|
<button type="button" data-view="infra2" data-module="infra2-soc" data-desk-nav class="nav-link">Infra 2 SOC</button>
|
||||||
|
<button type="button" data-view="email-migration" data-module="email-migration" id="nav-email-migration" data-desk-nav class="nav-link">Migração E-mail</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-zone__sep" aria-hidden="true"></div>
|
||||||
|
<div class="nav-zone" data-topnav-group="sistema">
|
||||||
|
<span class="nav-zone__label">Sistema</span>
|
||||||
|
<div class="nav-zone__links">
|
||||||
|
<button type="button" data-view="overview" data-module="overview" id="nav-overview" data-desk-nav class="nav-link">Overview</button>
|
||||||
|
<button type="button" data-view="account" data-module="core" id="nav-account" data-desk-nav class="nav-link">Minha conta</button>
|
||||||
|
<button type="button" data-view="messages" data-module="messages" id="nav-messages" hidden data-desk-nav class="nav-link">Mensagens</button>
|
||||||
|
<button type="button" data-view="admin" data-module="admin-users" id="nav-admin" hidden data-desk-nav class="nav-link">Administradores</button>
|
||||||
|
<button type="button" data-view="access-matrix" data-module="access-matrix" id="nav-access-matrix" hidden data-desk-nav class="nav-link">Matriz</button>
|
||||||
|
<button type="button" data-view="modules" data-module="modules-admin" id="nav-modules" hidden data-desk-nav class="nav-link">Módulos</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div class="app-chrome__meta">
|
||||||
|
<span id="header-user" class="header-user" hidden></span>
|
||||||
|
<span id="global-health" class="status-pill">…</span>
|
||||||
|
<button type="button" class="btn btn-ghost btn-sm" id="btn-logout" hidden>Sair</button>
|
||||||
|
<button type="button" class="btn btn-ghost btn-sm" id="btn-refresh">Atualizar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="workspace">
|
||||||
|
<aside class="context-sidebar" id="context-sidebar" hidden>
|
||||||
|
<div class="context-sidebar__head">
|
||||||
|
<p class="context-sidebar__label" id="context-sidebar-label">Processo</p>
|
||||||
|
<p class="context-sidebar__title" id="context-sidebar-title"></p>
|
||||||
|
</div>
|
||||||
|
<nav class="context-nav" id="context-nav" aria-label="Navegação do processo"></nav>
|
||||||
|
<div class="context-sidebar__foot">VM122 staging · v0.12.2-staging</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="workspace-main">
|
||||||
|
<main class="main">
|
||||||
|
<header class="page-header">
|
||||||
|
<div>
|
||||||
|
<h2 id="page-title">Dashboard</h2>
|
||||||
|
<p id="page-subtitle">Operações Ligbox — onboarding, tickets e monitoramento</p>
|
||||||
|
</div>
|
||||||
|
<div class="page-toolbar" id="page-toolbar"></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section id="view-dashboard" class="view">
|
||||||
|
<div id="dashboard-content"><p class="loading">Carregando…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-overview" class="view">
|
||||||
|
<div id="overview-content"><p class="loading">Carregando…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-overview-home" class="view">
|
||||||
|
<div id="overview-home-content"><p class="loading">Carregando…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-leads" class="view">
|
||||||
|
<div id="leads-content"><p class="loading">Carregando…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-tickets" class="view">
|
||||||
|
<div class="toolbar">
|
||||||
|
<button type="button" class="filter-btn active" data-filter="all">Todos</button>
|
||||||
|
<button type="button" class="filter-btn" data-filter="active">Activos</button>
|
||||||
|
<button type="button" class="filter-btn" data-filter="escalated">Escalados</button>
|
||||||
|
<button type="button" class="filter-btn" data-filter="assisting">Assistindo</button>
|
||||||
|
<button type="button" class="filter-btn" data-filter="closed">Fechados</button>
|
||||||
|
<button type="button" class="filter-btn" data-filter="leads" id="filter-leads" hidden>Leads</button>
|
||||||
|
<span class="toolbar-sep">|</span>
|
||||||
|
<button type="button" class="filter-btn active" data-source="all" data-kind="ticket">Todas origens</button>
|
||||||
|
<button type="button" class="filter-btn" data-source="vm112-onboard" data-kind="ticket">Onboard</button>
|
||||||
|
<button type="button" class="filter-btn" data-source="wazuh" data-kind="ticket">Wazuh</button>
|
||||||
|
</div>
|
||||||
|
<div class="grid-2">
|
||||||
|
<div id="ticket-list"><p class="loading">Carregando…</p></div>
|
||||||
|
<div id="ticket-detail">
|
||||||
|
<div class="card detail-panel"><p class="empty">Selecione um ticket para ver detalhes</p></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-events" class="view">
|
||||||
|
<div class="toolbar" id="events-toolbar">
|
||||||
|
<button type="button" class="filter-btn active" data-events-tab="webhooks">Webhooks</button>
|
||||||
|
<button type="button" class="filter-btn" data-events-tab="purges" id="events-tab-purges" hidden>Histórico de purges</button>
|
||||||
|
<button type="button" class="filter-btn" data-events-tab="security" id="events-tab-security" hidden>Segurança wizard</button>
|
||||||
|
<button type="button" class="filter-btn" data-events-tab="carbonio" id="events-tab-carbonio" data-module="carbonio-release" hidden>Bloqueios Carbonio</button>
|
||||||
|
<span class="toolbar-sep events-webhooks-only">|</span>
|
||||||
|
<button type="button" class="filter-btn active events-webhooks-only" data-source="all" data-kind="event">Todas origens</button>
|
||||||
|
<button type="button" class="filter-btn events-webhooks-only" data-source="vm112-onboard" data-kind="event">Onboard</button>
|
||||||
|
<button type="button" class="filter-btn events-webhooks-only" data-source="wazuh" data-kind="event">Wazuh</button>
|
||||||
|
</div>
|
||||||
|
<div id="events-content"><p class="loading">Carregando…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-email-migration" class="view" hidden>
|
||||||
|
<div id="email-migration-content"></div>
|
||||||
|
</section>
|
||||||
|
<section id="view-infra" class="view">
|
||||||
|
<div id="infra-content"><p class="loading">Carregando…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-infra2" class="view">
|
||||||
|
<div id="infra2-content"><p class="loading">Carregando SOC…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-agentic-ops" class="view">
|
||||||
|
<div id="agentic-ops-content"><p class="loading">Carregando Agent Squad…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-account" class="view">
|
||||||
|
<div id="account-content"><p class="loading">Carregando…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-messages" class="view">
|
||||||
|
<div id="messages-content"><p class="loading">Carregando…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-admin" class="view">
|
||||||
|
<div id="admin-content"><p class="loading">Carregando…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-access-matrix" class="view">
|
||||||
|
<div id="access-matrix-content"><p class="loading">Carregando matriz…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-modules" class="view">
|
||||||
|
<div id="modules-content"><p class="loading">Carregando…</p></div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="sidebar-user" hidden aria-hidden="true"></div>
|
||||||
|
<div id="team-drawer" class="team-drawer hidden" aria-hidden="true">
|
||||||
|
<div class="team-drawer-backdrop" data-close-team-drawer></div>
|
||||||
|
<aside class="team-drawer-panel" role="dialog" aria-modal="true" aria-labelledby="team-drawer-title">
|
||||||
|
<div class="team-drawer-header">
|
||||||
|
<h3 id="team-drawer-title">Editar membro</h3>
|
||||||
|
<button type="button" class="btn btn-ghost btn-sm" data-close-team-drawer>Fechar</button>
|
||||||
|
</div>
|
||||||
|
<div id="team-drawer-body" class="team-drawer-body"></div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
<div id="soc-test-modal" class="modal hidden" aria-hidden="true">
|
||||||
|
<div class="modal-backdrop" data-close-soc-test-modal></div>
|
||||||
|
<div class="modal-panel soc-test-modal-panel" role="dialog" aria-modal="true" aria-labelledby="soc-test-modal-title">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div>
|
||||||
|
<h3 id="soc-test-modal-title">Teste de webhook</h3>
|
||||||
|
<p id="soc-test-modal-sub" class="ticket-meta"></p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-ghost btn-sm" data-close-soc-test-modal>Fechar</button>
|
||||||
|
</div>
|
||||||
|
<div id="soc-test-modal-body" class="modal-body"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="overview-modal" class="modal hidden" aria-hidden="true">
|
||||||
|
<div class="modal-backdrop" data-close-overview-modal></div>
|
||||||
|
<div class="modal-panel modal-panel-lg" role="dialog" aria-modal="true" aria-labelledby="overview-modal-title">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div>
|
||||||
|
<h3 id="overview-modal-title">Detalhes do tenant</h3>
|
||||||
|
<p id="overview-modal-sub" class="ticket-meta"></p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-ghost btn-sm" data-close-overview-modal>Fechar</button>
|
||||||
|
</div>
|
||||||
|
<div id="overview-modal-body" class="modal-body"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="event-auditor-modal" class="modal hidden" aria-hidden="true">
|
||||||
|
<div class="modal-backdrop" data-close-event-auditor></div>
|
||||||
|
<div class="modal-panel modal-panel-xl event-auditor-panel" role="dialog" aria-modal="true" aria-labelledby="event-auditor-modal-title">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div>
|
||||||
|
<h3 id="event-auditor-modal-title">Auditor de Eventos</h3>
|
||||||
|
<p id="event-auditor-modal-sub" class="ticket-meta"></p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-ghost btn-sm" data-close-event-auditor>Fechar</button>
|
||||||
|
</div>
|
||||||
|
<div id="event-auditor-modal-body" class="modal-body"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="vm112-domain-modal" class="modal hidden" aria-hidden="true">
|
||||||
|
<div class="modal-backdrop" data-close-vm112-modal></div>
|
||||||
|
<div class="vm112-modal-shell" id="vm112-modal-shell">
|
||||||
|
<div class="modal-panel modal-panel-lg" role="dialog" aria-modal="true" aria-labelledby="vm112-domain-modal-title">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div>
|
||||||
|
<h3 id="vm112-domain-modal-title">Domínio VM112</h3>
|
||||||
|
<p id="vm112-domain-modal-sub" class="ticket-meta"></p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-ghost btn-sm" data-close-vm112-modal>Fechar</button>
|
||||||
|
</div>
|
||||||
|
<div id="vm112-domain-modal-body" class="modal-body"></div>
|
||||||
|
</div>
|
||||||
|
<aside id="vm112-purge-drawer" class="vm112-purge-drawer" aria-hidden="true" aria-label="Timeline purge">
|
||||||
|
<div class="vm112-purge-drawer-head">
|
||||||
|
<h4>Execução purge</h4>
|
||||||
|
<span id="vm112-purge-elapsed" class="vm112-purge-elapsed">—</span>
|
||||||
|
</div>
|
||||||
|
<ol id="vm112-purge-timeline" class="vm112-purge-timeline"></ol>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/assets/auth.js?v=20260626staging1"></script>
|
||||||
|
<script src="/assets/modules.js?v=20260619billing"></script>
|
||||||
|
<script src="/assets/billing-ui.js?v=20260619billing"></script>
|
||||||
|
<script src="/assets/servicos.js?v=20260626staging1"></script>
|
||||||
|
<script src="/assets/agentic-ops.js?v=20260623iconagentes"></script>
|
||||||
|
<script src="/assets/access-matrix.js?v=20260623matriz"></script>
|
||||||
|
<script src="/assets/topnav.js?v=20260626staging1"></script>
|
||||||
|
<script src="/assets/app.staging.js?v=20260626staging1"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4234
projects/ops-desk/frontend/staging-snapshot/stg-app.js
Normal file
4234
projects/ops-desk/frontend/staging-snapshot/stg-app.js
Normal file
File diff suppressed because it is too large
Load diff
437
projects/ops-desk/frontend/staging-snapshot/stg-index.html
Normal file
437
projects/ops-desk/frontend/staging-snapshot/stg-index.html
Normal file
|
|
@ -0,0 +1,437 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
<title>Ligbox Ops — Support Desk</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400..700&display=swap" rel="stylesheet"/>
|
||||||
|
<link rel="stylesheet" href="/assets/styles.css?v=20260625square"/>
|
||||||
|
<link rel="stylesheet" href="/assets/agentic-ops.css?v=20260623iconagentes"/>
|
||||||
|
<link rel="stylesheet" href="/assets/access-matrix.css?v=20260623matriz"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<svg width="0" height="0" style="position:absolute;visibility:hidden" aria-hidden="true" focusable="false">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="lg-dashboard" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#f6f6f8"/>
|
||||||
|
<stop offset="100%" stop-color="#d8d6dc"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="lg-overview" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#ff9fb5"/>
|
||||||
|
<stop offset="100%" stop-color="#8d233d"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="lg-ticket" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#ffd36d"/>
|
||||||
|
<stop offset="100%" stop-color="#e29b20"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="lg-target" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#ff9aa4"/>
|
||||||
|
<stop offset="100%" stop-color="#d11f42"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="lg-calendar" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#b38dff"/>
|
||||||
|
<stop offset="100%" stop-color="#6b42c9"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="lg-building" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#b9dbff"/>
|
||||||
|
<stop offset="100%" stop-color="#4c8fd5"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="lg-server" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#8b929e"/>
|
||||||
|
<stop offset="100%" stop-color="#4b4f58"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="lg-gear" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#f2f3f7"/>
|
||||||
|
<stop offset="100%" stop-color="#9aa0ab"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="lg-chat" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#6fc1ff"/>
|
||||||
|
<stop offset="100%" stop-color="#2f6ef0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="lg-admin" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stop-color="#c07aff"/>
|
||||||
|
<stop offset="100%" stop-color="#6c2eb9"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<filter id="nav-shadow" x="-25%" y="-25%" width="150%" height="150%">
|
||||||
|
<feDropShadow dx="0" dy="3" stdDeviation="2.5" flood-color="#000000" flood-opacity="0.28"/>
|
||||||
|
</filter>
|
||||||
|
|
||||||
|
<symbol id="icon-dashboard" viewBox="0 0 64 64">
|
||||||
|
<rect x="8" y="6" width="48" height="52" rx="10" fill="url(#lg-dashboard)" stroke="#b8b3bf" stroke-width="2"/>
|
||||||
|
<rect x="14" y="12" width="36" height="40" rx="6" fill="#f7fbff" stroke="#c9d2db"/>
|
||||||
|
<rect x="18" y="32" width="8" height="16" rx="2" fill="#2dc7b7"/>
|
||||||
|
<rect x="29" y="22" width="8" height="26" rx="2" fill="#57b5ff"/>
|
||||||
|
<rect x="40" y="16" width="8" height="32" rx="2" fill="#ef6179"/>
|
||||||
|
<rect x="12" y="8" width="44" height="6" rx="3" fill="#ffffff" opacity="0.55"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="icon-overview" viewBox="0 0 64 64">
|
||||||
|
<path d="M32 6 51 13v15c0 14-8.9 23.7-19 29-10.1-5.3-19-15-19-29V13L32 6Z" fill="url(#lg-overview)" stroke="#f1c2cf" stroke-width="2.2"/>
|
||||||
|
<path d="M32 13 45 18v10c0 9.5-5.8 16.5-13 20.8-7.2-4.3-13-11.3-13-20.8V18L32 13Z" fill="#fff3f6" opacity="0.85"/>
|
||||||
|
<path d="M20 18h24v10c0 8.6-5.2 14.9-12 18.9-6.8-4-12-10.3-12-18.9V18Z" fill="#8e1f3a"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="icon-tickets" viewBox="0 0 64 64">
|
||||||
|
<path d="M10 18a6 6 0 0 0 6-6h32a6 6 0 0 0 6 6v8a6 6 0 0 0 0 12v8a6 6 0 0 0-6 6H16a6 6 0 0 0-6-6v-8a6 6 0 0 0 0-12v-8Z" fill="url(#lg-ticket)" stroke="#d38b1a" stroke-width="2"/>
|
||||||
|
<path d="M24 18v28" stroke="#f9e0a9" stroke-width="2" stroke-dasharray="3 3"/>
|
||||||
|
<path d="m39 22 2.8 5.6 6.2.9-4.5 4.4 1.1 6.2-5.6-2.9-5.6 2.9 1.1-6.2-4.5-4.4 6.2-.9L39 22Z" fill="#7d4510"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="icon-leads" viewBox="0 0 64 64">
|
||||||
|
<circle cx="30" cy="34" r="20" fill="url(#lg-target)" stroke="#f1a5b1" stroke-width="2"/>
|
||||||
|
<circle cx="30" cy="34" r="13" fill="#ffd9df" opacity="0.88"/>
|
||||||
|
<circle cx="30" cy="34" r="7" fill="#fff"/>
|
||||||
|
<circle cx="30" cy="34" r="3.8" fill="#df2d51"/>
|
||||||
|
<path d="m37 27 13-9 4 4-9 13-1.6-6.5L37 27Z" fill="#4ba3ff" stroke="#2d6fd1" stroke-width="1.4" stroke-linejoin="round"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="icon-events" viewBox="0 0 64 64">
|
||||||
|
<rect x="10" y="14" width="44" height="38" rx="9" fill="url(#lg-calendar)" stroke="#cdb8ff" stroke-width="2"/>
|
||||||
|
<rect x="10" y="14" width="44" height="10" rx="9" fill="#f7efff" opacity="0.9"/>
|
||||||
|
<path d="M20 10v10M44 10v10" stroke="#7c52dd" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
<g fill="#efe7ff">
|
||||||
|
<rect x="18" y="28" width="6" height="6" rx="1.2"/>
|
||||||
|
<rect x="28" y="28" width="6" height="6" rx="1.2"/>
|
||||||
|
<rect x="38" y="28" width="6" height="6" rx="1.2"/>
|
||||||
|
<rect x="18" y="38" width="6" height="6" rx="1.2"/>
|
||||||
|
<rect x="28" y="38" width="6" height="6" rx="1.2"/>
|
||||||
|
</g>
|
||||||
|
<circle cx="47" cy="46" r="9" fill="#8a57da" stroke="#e6d8ff" stroke-width="1.8"/>
|
||||||
|
<path d="m47 40 1.7 3.5 3.8.6-2.7 2.6.6 3.8-3.4-1.8-3.4 1.8.6-3.8-2.7-2.6 3.8-.6L47 40Z" fill="#fff3a7"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="icon-tenants" viewBox="0 0 64 64">
|
||||||
|
<rect x="14" y="10" width="28" height="42" rx="4" fill="url(#lg-building)" stroke="#7ab3ea" stroke-width="2"/>
|
||||||
|
<rect x="28" y="18" width="22" height="34" rx="4" fill="#98cbf7" stroke="#5f9ed8" stroke-width="2"/>
|
||||||
|
<g fill="#eaf6ff">
|
||||||
|
<rect x="19" y="16" width="5" height="5" rx="1"/>
|
||||||
|
<rect x="27" y="16" width="5" height="5" rx="1"/>
|
||||||
|
<rect x="19" y="25" width="5" height="5" rx="1"/>
|
||||||
|
<rect x="27" y="25" width="5" height="5" rx="1"/>
|
||||||
|
<rect x="19" y="34" width="5" height="5" rx="1"/>
|
||||||
|
<rect x="27" y="34" width="5" height="5" rx="1"/>
|
||||||
|
<rect x="33" y="23" width="5" height="5" rx="1"/>
|
||||||
|
<rect x="41" y="23" width="5" height="5" rx="1"/>
|
||||||
|
<rect x="33" y="32" width="5" height="5" rx="1"/>
|
||||||
|
<rect x="41" y="32" width="5" height="5" rx="1"/>
|
||||||
|
</g>
|
||||||
|
<rect x="29" y="42" width="8" height="10" rx="1.5" fill="#5a7ca0"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="icon-infra" viewBox="0 0 64 64">
|
||||||
|
<rect x="16" y="8" width="32" height="48" rx="6" fill="url(#lg-server)" stroke="#969daa" stroke-width="2"/>
|
||||||
|
<g fill="#2a2f38" stroke="#6c727d" stroke-width="1">
|
||||||
|
<rect x="20" y="14" width="24" height="10" rx="2"/>
|
||||||
|
<rect x="20" y="28" width="24" height="10" rx="2"/>
|
||||||
|
<rect x="20" y="42" width="24" height="8" rx="2"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<circle cx="25" cy="19" r="1.8" fill="#ff5f72"/>
|
||||||
|
<circle cx="25" cy="33" r="1.8" fill="#ff5f72"/>
|
||||||
|
<circle cx="25" cy="46" r="1.6" fill="#ff5f72"/>
|
||||||
|
<rect x="30" y="18" width="10" height="2" rx="1" fill="#d9dde5"/>
|
||||||
|
<rect x="30" y="32" width="10" height="2" rx="1" fill="#d9dde5"/>
|
||||||
|
<rect x="30" y="45" width="10" height="2" rx="1" fill="#d9dde5"/>
|
||||||
|
</g>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="icon-account" viewBox="0 0 64 64">
|
||||||
|
<circle cx="32" cy="32" r="10" fill="#872338"/>
|
||||||
|
<path d="M32 10 36.5 14.1 42.7 13.3 45 19.1 50.9 21.4 50.1 27.6 54.2 32 50.1 36.4 50.9 42.6 45 44.9 42.7 50.7 36.5 49.9 32 54 27.5 49.9 21.3 50.7 19 44.9 13.1 42.6 13.9 36.4 9.8 32 13.9 27.6 13.1 21.4 19 19.1 21.3 13.3 27.5 14.1Z" fill="url(#lg-gear)" stroke="#b9bec8" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
<circle cx="32" cy="32" r="11.5" fill="none" stroke="#dfe2e8" stroke-width="2"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="icon-messages" viewBox="0 0 64 64">
|
||||||
|
<path d="M15 14h26c6.1 0 11 4.9 11 11v9c0 6.1-4.9 11-11 11H31l-10 8v-8h-6c-6.1 0-11-4.9-11-11v-9c0-6.1 4.9-11 11-11Z" fill="url(#lg-chat)" stroke="#7fccff" stroke-width="2"/>
|
||||||
|
<rect x="16" y="24" width="20" height="3.4" rx="1.7" fill="#eef7ff"/>
|
||||||
|
<rect x="16" y="31" width="24" height="3.4" rx="1.7" fill="#eef7ff"/>
|
||||||
|
<rect x="16" y="38" width="17" height="3.4" rx="1.7" fill="#eef7ff"/>
|
||||||
|
<circle cx="48" cy="18" r="7" fill="#ff6d85" stroke="#ffb1be" stroke-width="2"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="icon-admin" viewBox="0 0 64 64">
|
||||||
|
<circle cx="24" cy="25" r="11" fill="url(#lg-admin)" stroke="#d8b7ff" stroke-width="2"/>
|
||||||
|
<circle cx="41" cy="26" r="10" fill="#8a45dd" stroke="#d8b7ff" stroke-width="2"/>
|
||||||
|
<path d="M10 51c0-8.3 6.7-15 15-15h1c8.3 0 15 6.7 15 15v3H10v-3Z" fill="#7a34cb"/>
|
||||||
|
<path d="M30 52c0-7.2 5.8-13 13-13s13 5.8 13 13v2H30v-2Z" fill="#5c27a3"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="icon-overview-home" viewBox="0 0 64 64">
|
||||||
|
<rect x="8" y="10" width="48" height="44" rx="8" fill="#eef4ff" stroke="#9bbcff" stroke-width="2"/>
|
||||||
|
<rect x="14" y="16" width="14" height="12" rx="3" fill="#2f6fed" opacity="0.9"/>
|
||||||
|
<rect x="32" y="16" width="18" height="12" rx="3" fill="#dbeafe"/>
|
||||||
|
<rect x="14" y="32" width="36" height="16" rx="3" fill="#fff" stroke="#c7d7fe"/>
|
||||||
|
<path d="M14 32h36" stroke="#c7d7fe"/>
|
||||||
|
<rect x="18" y="36" width="10" height="2.5" rx="1" fill="#64748b"/>
|
||||||
|
<rect x="32" y="36" width="14" height="2.5" rx="1" fill="#64748b"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
|
<symbol id="icon-infra2" viewBox="0 0 64 64">
|
||||||
|
<rect x="6" y="8" width="52" height="48" rx="8" fill="#0f172a" stroke="#38bdf8" stroke-width="2"/>
|
||||||
|
<circle cx="32" cy="32" r="14" fill="none" stroke="#22d3ee" stroke-width="2" opacity="0.5"/>
|
||||||
|
<circle cx="32" cy="32" r="8" fill="none" stroke="#38bdf8" stroke-width="1.5" opacity="0.7"/>
|
||||||
|
<path d="M32 18v28M18 32h28" stroke="#38bdf8" stroke-width="1" opacity="0.35"/>
|
||||||
|
<circle cx="32" cy="32" r="3" fill="#22d3ee"/>
|
||||||
|
<circle cx="44" cy="22" r="4" fill="#ef4444" stroke="#fecaca" stroke-width="1.2"/>
|
||||||
|
<path d="M36 28 L41 24" stroke="#f87171" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<rect x="12" y="12" width="8" height="3" rx="1" fill="#22c55e" opacity="0.9"/>
|
||||||
|
<rect x="12" y="49" width="14" height="3" rx="1" fill="#64748b"/>
|
||||||
|
</symbol>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
<div class="shell shell--v2 shell--no-context">
|
||||||
|
<header class="app-chrome app-chrome--modern">
|
||||||
|
<div class="app-chrome__inner">
|
||||||
|
<div class="app-chrome__brand">
|
||||||
|
<span class="app-chrome__mark" aria-hidden="true"></span>
|
||||||
|
<h1>Ligbox <span class="brand-ops">Ops</span></h1>
|
||||||
|
</div>
|
||||||
|
<nav class="app-chrome__nav" aria-label="Módulos Desk">
|
||||||
|
<div class="nav-zone" data-topnav-group="operacoes">
|
||||||
|
<span class="nav-zone__label">Operações</span>
|
||||||
|
<div class="nav-zone__links">
|
||||||
|
<button type="button" data-view="agentic-ops" data-module="agentic-ops" id="nav-agentic-ops" data-desk-nav class="nav-link">Agentic Ops</button>
|
||||||
|
<button type="button" data-view="dashboard" data-module="core" data-desk-nav class="nav-link">Dashboard</button>
|
||||||
|
<button type="button" data-view="tickets" data-module="core" data-desk-nav class="nav-link">Tickets</button>
|
||||||
|
<button type="button" data-view="events" data-module="events" id="nav-events" data-desk-nav class="nav-link">Eventos</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-zone__sep" aria-hidden="true"></div>
|
||||||
|
<div class="nav-zone" data-topnav-group="comercial">
|
||||||
|
<span class="nav-zone__label">Comercial</span>
|
||||||
|
<div class="nav-zone__links">
|
||||||
|
<button type="button" data-view="overview-home" data-module="overview-home" id="nav-overview-home" data-desk-nav class="nav-link">Serviços</button>
|
||||||
|
<button type="button" data-view="leads" data-module="leads" id="nav-leads" hidden data-desk-nav class="nav-link">Leads</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-zone__sep" aria-hidden="true"></div>
|
||||||
|
<a id="nav-console-return" class="nav-zone nav-zone--console" href="https://console.ligbox.com.br">
|
||||||
|
<span class="nav-zone__label">Plataforma</span>
|
||||||
|
<span class="nav-link nav-link--console">Console</span>
|
||||||
|
</a>
|
||||||
|
<div class="nav-zone__sep" aria-hidden="true"></div>
|
||||||
|
<div class="nav-zone" data-topnav-group="infra">
|
||||||
|
<span class="nav-zone__label">Infra</span>
|
||||||
|
<div class="nav-zone__links">
|
||||||
|
<button type="button" data-view="infra" data-module="infra" data-desk-nav class="nav-link">Infra</button>
|
||||||
|
<button type="button" data-view="infra2" data-module="infra2-soc" data-desk-nav class="nav-link">Infra 2 SOC</button>
|
||||||
|
<button type="button" data-view="email-migration" data-module="email-migration" id="nav-email-migration" data-desk-nav class="nav-link">Migração E-mail</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-zone__sep" aria-hidden="true"></div>
|
||||||
|
<div class="nav-zone" data-topnav-group="sistema">
|
||||||
|
<span class="nav-zone__label">Sistema</span>
|
||||||
|
<div class="nav-zone__links">
|
||||||
|
<button type="button" data-view="overview" data-module="overview" id="nav-overview" data-desk-nav class="nav-link">Overview</button>
|
||||||
|
<button type="button" data-view="account" data-module="core" id="nav-account" data-desk-nav class="nav-link">Minha conta</button>
|
||||||
|
<button type="button" data-view="messages" data-module="messages" id="nav-messages" hidden data-desk-nav class="nav-link">Mensagens</button>
|
||||||
|
<button type="button" data-view="admin" data-module="admin-users" id="nav-admin" hidden data-desk-nav class="nav-link">Administradores</button>
|
||||||
|
<button type="button" data-view="access-matrix" data-module="access-matrix" id="nav-access-matrix" hidden data-desk-nav class="nav-link">Matriz</button>
|
||||||
|
<button type="button" data-view="modules" data-module="modules-admin" id="nav-modules" hidden data-desk-nav class="nav-link">Módulos</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<div class="app-chrome__meta">
|
||||||
|
<span id="header-user" class="header-user" hidden></span>
|
||||||
|
<span id="global-health" class="status-pill">…</span>
|
||||||
|
<button type="button" class="btn btn-ghost btn-sm" id="btn-logout" hidden>Sair</button>
|
||||||
|
<button type="button" class="btn btn-ghost btn-sm" id="btn-refresh">Atualizar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="workspace">
|
||||||
|
<aside class="context-sidebar" id="context-sidebar" hidden>
|
||||||
|
<div class="context-sidebar__head">
|
||||||
|
<p class="context-sidebar__label" id="context-sidebar-label">Processo</p>
|
||||||
|
<p class="context-sidebar__title" id="context-sidebar-title"></p>
|
||||||
|
</div>
|
||||||
|
<nav class="context-nav" id="context-nav" aria-label="Navegação do processo"></nav>
|
||||||
|
<div class="context-sidebar__foot">VM122 · v0.12.1-square</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="workspace-main">
|
||||||
|
<main class="main">
|
||||||
|
<header class="page-header">
|
||||||
|
<div>
|
||||||
|
<h2 id="page-title">Dashboard</h2>
|
||||||
|
<p id="page-subtitle">Operações Ligbox — onboarding, tickets e monitoramento</p>
|
||||||
|
</div>
|
||||||
|
<div class="page-toolbar" id="page-toolbar"></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section id="view-dashboard" class="view">
|
||||||
|
<div id="dashboard-content"><p class="loading">Carregando…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-overview" class="view">
|
||||||
|
<div id="overview-content"><p class="loading">Carregando…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-overview-home" class="view">
|
||||||
|
<div id="overview-home-content"><p class="loading">Carregando…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-leads" class="view">
|
||||||
|
<div id="leads-content"><p class="loading">Carregando…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-tickets" class="view">
|
||||||
|
<div class="toolbar">
|
||||||
|
<button type="button" class="filter-btn active" data-filter="all">Todos</button>
|
||||||
|
<button type="button" class="filter-btn" data-filter="active">Activos</button>
|
||||||
|
<button type="button" class="filter-btn" data-filter="escalated">Escalados</button>
|
||||||
|
<button type="button" class="filter-btn" data-filter="assisting">Assistindo</button>
|
||||||
|
<button type="button" class="filter-btn" data-filter="closed">Fechados</button>
|
||||||
|
<button type="button" class="filter-btn" data-filter="leads" id="filter-leads" hidden>Leads</button>
|
||||||
|
<span class="toolbar-sep">|</span>
|
||||||
|
<button type="button" class="filter-btn active" data-source="all" data-kind="ticket">Todas origens</button>
|
||||||
|
<button type="button" class="filter-btn" data-source="vm112-onboard" data-kind="ticket">Onboard</button>
|
||||||
|
<button type="button" class="filter-btn" data-source="wazuh" data-kind="ticket">Wazuh</button>
|
||||||
|
</div>
|
||||||
|
<div class="grid-2">
|
||||||
|
<div id="ticket-list"><p class="loading">Carregando…</p></div>
|
||||||
|
<div id="ticket-detail">
|
||||||
|
<div class="card detail-panel"><p class="empty">Selecione um ticket para ver detalhes</p></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-events" class="view">
|
||||||
|
<div class="toolbar" id="events-toolbar">
|
||||||
|
<button type="button" class="filter-btn active" data-events-tab="webhooks">Webhooks</button>
|
||||||
|
<button type="button" class="filter-btn" data-events-tab="purges" id="events-tab-purges" hidden>Histórico de purges</button>
|
||||||
|
<button type="button" class="filter-btn" data-events-tab="security" id="events-tab-security" hidden>Segurança wizard</button>
|
||||||
|
<button type="button" class="filter-btn" data-events-tab="carbonio" id="events-tab-carbonio" data-module="carbonio-release" hidden>Bloqueios Carbonio</button>
|
||||||
|
<span class="toolbar-sep events-webhooks-only">|</span>
|
||||||
|
<button type="button" class="filter-btn active events-webhooks-only" data-source="all" data-kind="event">Todas origens</button>
|
||||||
|
<button type="button" class="filter-btn events-webhooks-only" data-source="vm112-onboard" data-kind="event">Onboard</button>
|
||||||
|
<button type="button" class="filter-btn events-webhooks-only" data-source="wazuh" data-kind="event">Wazuh</button>
|
||||||
|
</div>
|
||||||
|
<div id="events-content"><p class="loading">Carregando…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-email-migration" class="view" hidden>
|
||||||
|
<div id="email-migration-content"></div>
|
||||||
|
</section>
|
||||||
|
<section id="view-infra" class="view">
|
||||||
|
<div id="infra-content"><p class="loading">Carregando…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-infra2" class="view">
|
||||||
|
<div id="infra2-content"><p class="loading">Carregando SOC…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-agentic-ops" class="view">
|
||||||
|
<div id="agentic-ops-content"><p class="loading">Carregando Agent Squad…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-account" class="view">
|
||||||
|
<div id="account-content"><p class="loading">Carregando…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-messages" class="view">
|
||||||
|
<div id="messages-content"><p class="loading">Carregando…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-admin" class="view">
|
||||||
|
<div id="admin-content"><p class="loading">Carregando…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-access-matrix" class="view">
|
||||||
|
<div id="access-matrix-content"><p class="loading">Carregando matriz…</p></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="view-modules" class="view">
|
||||||
|
<div id="modules-content"><p class="loading">Carregando…</p></div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="sidebar-user" hidden aria-hidden="true"></div>
|
||||||
|
<div id="team-drawer" class="team-drawer hidden" aria-hidden="true">
|
||||||
|
<div class="team-drawer-backdrop" data-close-team-drawer></div>
|
||||||
|
<aside class="team-drawer-panel" role="dialog" aria-modal="true" aria-labelledby="team-drawer-title">
|
||||||
|
<div class="team-drawer-header">
|
||||||
|
<h3 id="team-drawer-title">Editar membro</h3>
|
||||||
|
<button type="button" class="btn btn-ghost btn-sm" data-close-team-drawer>Fechar</button>
|
||||||
|
</div>
|
||||||
|
<div id="team-drawer-body" class="team-drawer-body"></div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
<div id="soc-test-modal" class="modal hidden" aria-hidden="true">
|
||||||
|
<div class="modal-backdrop" data-close-soc-test-modal></div>
|
||||||
|
<div class="modal-panel soc-test-modal-panel" role="dialog" aria-modal="true" aria-labelledby="soc-test-modal-title">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div>
|
||||||
|
<h3 id="soc-test-modal-title">Teste de webhook</h3>
|
||||||
|
<p id="soc-test-modal-sub" class="ticket-meta"></p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-ghost btn-sm" data-close-soc-test-modal>Fechar</button>
|
||||||
|
</div>
|
||||||
|
<div id="soc-test-modal-body" class="modal-body"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="overview-modal" class="modal hidden" aria-hidden="true">
|
||||||
|
<div class="modal-backdrop" data-close-overview-modal></div>
|
||||||
|
<div class="modal-panel modal-panel-lg" role="dialog" aria-modal="true" aria-labelledby="overview-modal-title">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div>
|
||||||
|
<h3 id="overview-modal-title">Detalhes do tenant</h3>
|
||||||
|
<p id="overview-modal-sub" class="ticket-meta"></p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-ghost btn-sm" data-close-overview-modal>Fechar</button>
|
||||||
|
</div>
|
||||||
|
<div id="overview-modal-body" class="modal-body"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="event-auditor-modal" class="modal hidden" aria-hidden="true">
|
||||||
|
<div class="modal-backdrop" data-close-event-auditor></div>
|
||||||
|
<div class="modal-panel modal-panel-xl event-auditor-panel" role="dialog" aria-modal="true" aria-labelledby="event-auditor-modal-title">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div>
|
||||||
|
<h3 id="event-auditor-modal-title">Auditor de Eventos</h3>
|
||||||
|
<p id="event-auditor-modal-sub" class="ticket-meta"></p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-ghost btn-sm" data-close-event-auditor>Fechar</button>
|
||||||
|
</div>
|
||||||
|
<div id="event-auditor-modal-body" class="modal-body"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="vm112-domain-modal" class="modal hidden" aria-hidden="true">
|
||||||
|
<div class="modal-backdrop" data-close-vm112-modal></div>
|
||||||
|
<div class="vm112-modal-shell" id="vm112-modal-shell">
|
||||||
|
<div class="modal-panel modal-panel-lg" role="dialog" aria-modal="true" aria-labelledby="vm112-domain-modal-title">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div>
|
||||||
|
<h3 id="vm112-domain-modal-title">Domínio VM112</h3>
|
||||||
|
<p id="vm112-domain-modal-sub" class="ticket-meta"></p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-ghost btn-sm" data-close-vm112-modal>Fechar</button>
|
||||||
|
</div>
|
||||||
|
<div id="vm112-domain-modal-body" class="modal-body"></div>
|
||||||
|
</div>
|
||||||
|
<aside id="vm112-purge-drawer" class="vm112-purge-drawer" aria-hidden="true" aria-label="Timeline purge">
|
||||||
|
<div class="vm112-purge-drawer-head">
|
||||||
|
<h4>Execução purge</h4>
|
||||||
|
<span id="vm112-purge-elapsed" class="vm112-purge-elapsed">—</span>
|
||||||
|
</div>
|
||||||
|
<ol id="vm112-purge-timeline" class="vm112-purge-timeline"></ol>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/assets/auth.js?v=20260619billing"></script>
|
||||||
|
<script src="/assets/modules.js?v=20260619billing"></script>
|
||||||
|
<script src="/assets/billing-ui.js?v=20260619billing"></script>
|
||||||
|
<script src="/assets/servicos.js?v=20260625mospdelete"></script>
|
||||||
|
<script src="/assets/agentic-ops.js?v=20260623iconagentes"></script>
|
||||||
|
<script src="/assets/access-matrix.js?v=20260623matriz"></script>
|
||||||
|
<script src="/assets/topnav.js?v=20260625modern"></script>
|
||||||
|
<script src="/assets/app.js?v=20260625modern"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
926
projects/ops-desk/frontend/staging-snapshot/stg-servicos.js
Normal file
926
projects/ops-desk/frontend/staging-snapshot/stg-servicos.js
Normal file
|
|
@ -0,0 +1,926 @@
|
||||||
|
/**
|
||||||
|
* Serviços — Orquestração multi-produto (Spec 018)
|
||||||
|
* Fase 1: clientes derivados VM112 + tiles cPanel + purge Spec 017.
|
||||||
|
*/
|
||||||
|
const DeskServices = (() => {
|
||||||
|
const API_BASE = '/api';
|
||||||
|
let _domains = [];
|
||||||
|
let _clients = [];
|
||||||
|
let _query = '';
|
||||||
|
let _selectedClientId = null;
|
||||||
|
let _selectedServiceId = 'email_tenant';
|
||||||
|
let _billingByDomain = {};
|
||||||
|
|
||||||
|
const OPS_SCOPES = [
|
||||||
|
{ id: 'carbonio', label: 'Carbonio', desc: 'Domínio e contas de e-mail (zmprov)' },
|
||||||
|
{ id: 'site', label: 'ligbox-sites', desc: 'Pasta do tenant em /opt/ligbox-sites/domains/' },
|
||||||
|
{ id: 'portal', label: 'Portal users', desc: 'Contas Self-Service ligadas ao domínio' },
|
||||||
|
{ id: 'cloudflare', label: 'Cloudflare', desc: 'Zona DNS na conta Ibytera' },
|
||||||
|
{ id: 'traefik', label: 'Traefik / SNI', desc: 'Routers mail.* no CT114' },
|
||||||
|
{ id: 'desk', label: 'Desk OPS', desc: 'Webhooks, tickets e audit_domains' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const SERVICE_CATALOG = [
|
||||||
|
{
|
||||||
|
id: 'email_tenant',
|
||||||
|
label: 'E-mail Tenant',
|
||||||
|
desc: 'Domínio partilhado no Carbonio VM112',
|
||||||
|
icon: '✉',
|
||||||
|
wizard: 'vm112-mail',
|
||||||
|
wizardHost: 'VM112',
|
||||||
|
deliveryModel: 'saas',
|
||||||
|
category: 'apps',
|
||||||
|
phase: 'active',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'site_cms',
|
||||||
|
label: 'Site / CMS',
|
||||||
|
desc: 'Pasta ligbox-sites do domínio',
|
||||||
|
icon: '🌐',
|
||||||
|
wizard: 'vm112-mail',
|
||||||
|
wizardHost: 'VM112',
|
||||||
|
deliveryModel: 'saas',
|
||||||
|
category: 'apps',
|
||||||
|
phase: 'active',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mail_dedicated',
|
||||||
|
label: 'Servidor E-mail Dedicado',
|
||||||
|
desc: 'VM ou CT exclusivo — wizard próprio (Proxmox)',
|
||||||
|
icon: '🖥',
|
||||||
|
wizard: null,
|
||||||
|
wizardHost: 'futuro',
|
||||||
|
deliveryModel: 'saas',
|
||||||
|
category: 'apps',
|
||||||
|
phase: 'planned',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'firewall',
|
||||||
|
label: 'Firewall',
|
||||||
|
desc: 'pfSense — NAT, regras, WAN',
|
||||||
|
icon: '🛡',
|
||||||
|
wizard: 'wizard-iaas-fw',
|
||||||
|
wizardHost: 'futuro',
|
||||||
|
deliveryModel: 'iaas',
|
||||||
|
category: 'security',
|
||||||
|
phase: 'planned',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cloud',
|
||||||
|
label: 'Cloud / IaaS',
|
||||||
|
desc: 'VPS gerenciado no Proxmox',
|
||||||
|
icon: '☁',
|
||||||
|
wizard: 'wizard-iaas-vps',
|
||||||
|
wizardHost: 'futuro',
|
||||||
|
deliveryModel: 'iaas',
|
||||||
|
category: 'infra',
|
||||||
|
phase: 'planned',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'wazuh_domain',
|
||||||
|
label: 'Wazuh por domínio',
|
||||||
|
desc: 'SOC e agentes — wizard próprio',
|
||||||
|
icon: '👁',
|
||||||
|
wizard: 'wizard-soc-wazuh',
|
||||||
|
wizardHost: 'futuro',
|
||||||
|
deliveryModel: 'saas',
|
||||||
|
category: 'security',
|
||||||
|
phase: 'planned',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const CATEGORY_LABELS = {
|
||||||
|
apps: 'Aplicações (MOSP)',
|
||||||
|
infra: 'Infraestrutura',
|
||||||
|
security: 'Segurança',
|
||||||
|
};
|
||||||
|
|
||||||
|
const DELIVERY_LABELS = {
|
||||||
|
saas: 'SaaS',
|
||||||
|
iaas: 'IaaS',
|
||||||
|
paas: 'PaaS',
|
||||||
|
traditional: 'Suporte',
|
||||||
|
};
|
||||||
|
|
||||||
|
function canAccess() {
|
||||||
|
if (window.DeskModules?.loaded && !window.DeskModules.isEnabled('overview-home')) return false;
|
||||||
|
return typeof canManageVm112Domains === 'function' && canManageVm112Domains();
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc(s) {
|
||||||
|
return String(s ?? '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFetchError(err) {
|
||||||
|
const msg = String(err?.message || err || '');
|
||||||
|
if (err?.name === 'AbortError' || msg.includes('aborted') || msg.includes('Failed to fetch')) {
|
||||||
|
return 'VM112 não respondeu a tempo — o wizard pode estar sobrecarregado ou a reiniciar. Aguarde 1–2 min e clique «Tentar de novo».';
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apiFetch(path, options = {}, timeoutMs = 60000) {
|
||||||
|
let res;
|
||||||
|
try {
|
||||||
|
res = await fetchWithTimeout(`${API_BASE}${path}`, {
|
||||||
|
headers: authHeaders({ 'Content-Type': 'application/json', ...(options.headers || {}) }),
|
||||||
|
...options,
|
||||||
|
}, timeoutMs);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(formatFetchError(err));
|
||||||
|
}
|
||||||
|
if (res.status === 401) { logout(); throw new Error('sessão expirada'); }
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
const detail = data.detail;
|
||||||
|
let errText = typeof detail === 'string' ? detail : JSON.stringify(detail || `${res.status}`);
|
||||||
|
if (res.status === 504) {
|
||||||
|
errText = '504 Gateway Timeout — o purge pode demorar vários minutos. Verifique na VM112 se concluiu antes de repetir.';
|
||||||
|
} else if (res.status === 409) {
|
||||||
|
errText = typeof detail === 'string'
|
||||||
|
? detail
|
||||||
|
: 'Purge já em curso para este domínio — aguarde a conclusão no drawer lateral.';
|
||||||
|
} else if (res.status === 503) {
|
||||||
|
errText = typeof detail === 'string'
|
||||||
|
? detail
|
||||||
|
: 'Servidor ocupado — aguarde alguns segundos e tente novamente.';
|
||||||
|
} else if (res.status === 500 && errText === '500') {
|
||||||
|
errText = 'Erro interno ao iniciar purge — verifique logs da API Desk (VM122).';
|
||||||
|
}
|
||||||
|
throw new Error(errText);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildClients(domains) {
|
||||||
|
return domains.map((d) => ({
|
||||||
|
id: d.domain,
|
||||||
|
domain: d.domain,
|
||||||
|
displayName: d.domain,
|
||||||
|
subtitle: d.portal_admin_email || 'sem admin portal',
|
||||||
|
health: d.carbonio_exists ? 'ok' : 'warn',
|
||||||
|
raw: d,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBillingMap() {
|
||||||
|
if (!window.DeskModules?.isEnabled('billing-recurrence')) return;
|
||||||
|
try {
|
||||||
|
const data = await apiFetch('/v1/billing/accounts?limit=500');
|
||||||
|
_billingByDomain = {};
|
||||||
|
for (const a of data.accounts || []) _billingByDomain[a.domain] = a;
|
||||||
|
} catch { _billingByDomain = {}; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDomains() {
|
||||||
|
const data = await apiFetch('/v1/vm112/domains', {}, 120000);
|
||||||
|
_domains = data.domains || [];
|
||||||
|
await loadBillingMap();
|
||||||
|
_clients = buildClients(_domains);
|
||||||
|
if (_selectedClientId && !_clients.some((c) => c.id === _selectedClientId)) {
|
||||||
|
_selectedClientId = null;
|
||||||
|
}
|
||||||
|
return _domains;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filteredClients() {
|
||||||
|
const q = _query.trim().toLowerCase();
|
||||||
|
if (!q) return _clients;
|
||||||
|
return _clients.filter((c) => {
|
||||||
|
const blob = [
|
||||||
|
c.domain,
|
||||||
|
c.subtitle,
|
||||||
|
c.raw.mail_host,
|
||||||
|
(c.raw.portal_users || []).map((u) => u.login_id).join(' '),
|
||||||
|
].join(' ').toLowerCase();
|
||||||
|
return blob.includes(q);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedClient() {
|
||||||
|
return _clients.find((c) => c.id === _selectedClientId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function serviceStatus(serviceId, client) {
|
||||||
|
if (!client) return 'inactive';
|
||||||
|
const d = client.raw;
|
||||||
|
if (serviceId === 'email_tenant') {
|
||||||
|
if (d.carbonio_exists || d.site_folder_exists || (d.portal_users || []).length) return 'active';
|
||||||
|
return 'inactive';
|
||||||
|
}
|
||||||
|
if (serviceId === 'site_cms') {
|
||||||
|
return d.site_folder_exists ? 'active' : 'inactive';
|
||||||
|
}
|
||||||
|
const cat = SERVICE_CATALOG.find((s) => s.id === serviceId);
|
||||||
|
return cat?.phase === 'planned' ? 'planned' : 'inactive';
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(status) {
|
||||||
|
if (status === 'active') return 'Activo';
|
||||||
|
if (status === 'planned') return 'Em breve';
|
||||||
|
return 'Não contratado';
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeOfferCount(client) {
|
||||||
|
return SERVICE_CATALOG.filter((s) => serviceStatus(s.id, client) === 'active').length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function statsHtml() {
|
||||||
|
const billingActive = Object.values(_billingByDomain).filter((a) => a.recurrence_active).length;
|
||||||
|
const total = _clients.length;
|
||||||
|
const emailActive = _clients.filter((c) => serviceStatus('email_tenant', c) === 'active').length;
|
||||||
|
const sites = _clients.filter((c) => c.raw.site_folder_exists).length;
|
||||||
|
const logins = _clients.reduce((n, c) => n + (c.raw.portal_users || []).length, 0);
|
||||||
|
return `
|
||||||
|
<div class="servicos-stat"><strong>${total}</strong><span>clientes</span></div>
|
||||||
|
<div class="servicos-stat"><strong>${emailActive}</strong><span>tenant e-mail ativo</span></div>
|
||||||
|
<div class="servicos-stat"><strong>${sites}</strong><span>tenant site CMS</span></div>
|
||||||
|
<div class="servicos-stat"><strong>${logins}</strong><span>logins portal</span></div>
|
||||||
|
<div class="servicos-stat"><strong>${billingActive}</strong><span>recorrências</span></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clientRow(c) {
|
||||||
|
const sel = _selectedClientId === c.id ? ' servicos-client-row--selected' : '';
|
||||||
|
const emailSt = serviceStatus('email_tenant', c);
|
||||||
|
const offers = activeOfferCount(c);
|
||||||
|
return `
|
||||||
|
<button type="button" class="servicos-client-row${sel}" data-client-id="${esc(c.id)}">
|
||||||
|
<span class="servicos-client-dot ${c.health}"></span>
|
||||||
|
<div class="servicos-client-main">
|
||||||
|
<strong>${esc(c.displayName)}</strong>
|
||||||
|
<span>${esc(c.subtitle)} · ${offers} tenant(s) de oferta</span>
|
||||||
|
</div>
|
||||||
|
<span class="servicos-client-badge servicos-client-badge--${emailSt}">${statusLabel(emailSt)}</span>
|
||||||
|
${_billingByDomain[c.domain]?.recurrence_active ? `<span class="servicos-billing-badge servicos-billing-badge--active" data-billing-domain="${esc(c.domain)}" title="Recorrência ativa">💳</span>` : (_billingByDomain[c.domain] ? `<span class="servicos-billing-badge" data-billing-domain="${esc(c.domain)}" title="Cobrança">💳</span>` : '')}
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clientsListHtml() {
|
||||||
|
const rows = filteredClients();
|
||||||
|
return rows.length
|
||||||
|
? rows.map(clientRow).join('')
|
||||||
|
: '<p class="servicos-empty">Nenhum cliente encontrado.</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function serviceTile(service, client) {
|
||||||
|
const status = client ? serviceStatus(service.id, client) : 'inactive';
|
||||||
|
const clickable = status === 'active' && service.id === 'email_tenant';
|
||||||
|
const sel = _selectedServiceId === service.id ? ' servicos-tile--selected' : '';
|
||||||
|
const tag = statusLabel(status);
|
||||||
|
const dm = DELIVERY_LABELS[service.deliveryModel] || service.deliveryModel;
|
||||||
|
const wizNote = service.wizard
|
||||||
|
? `wizard: ${service.wizard} (${service.wizardHost})`
|
||||||
|
: (service.phase === 'planned' ? 'wizard próprio — planeamento MOSP' : '');
|
||||||
|
return `
|
||||||
|
<article class="servicos-tile servicos-tile--${status}${sel}${clickable ? ' servicos-tile--clickable' : ''}"
|
||||||
|
data-service-id="${esc(service.id)}"
|
||||||
|
data-clickable="${clickable ? '1' : '0'}">
|
||||||
|
<span class="servicos-tile-tier servicos-tile-tier--${esc(service.deliveryModel)}">${esc(dm)}</span>
|
||||||
|
<div class="servicos-tile-icon" aria-hidden="true">${service.icon}</div>
|
||||||
|
<h4 class="servicos-tile-title">${esc(service.label)}</h4>
|
||||||
|
<p class="servicos-tile-desc">${esc(service.desc)}</p>
|
||||||
|
<span class="servicos-tile-status servicos-tile-status--${status}">${tag}</span>
|
||||||
|
${wizNote ? `<span class="servicos-tile-wizard">${esc(wizNote)}</span>` : ''}
|
||||||
|
${clickable ? '<span class="servicos-tile-action">Gerir / Purge →</span>' : ''}
|
||||||
|
</article>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function servicesGridHtml() {
|
||||||
|
const client = selectedClient();
|
||||||
|
if (!client) {
|
||||||
|
return '<div class="servicos-empty servicos-empty--center">Seleccione um cliente à esquerda para ver os tenants de oferta.</div>';
|
||||||
|
}
|
||||||
|
const byCat = {};
|
||||||
|
for (const s of SERVICE_CATALOG) {
|
||||||
|
const cat = s.category || 'apps';
|
||||||
|
if (!byCat[cat]) byCat[cat] = [];
|
||||||
|
byCat[cat].push(s);
|
||||||
|
}
|
||||||
|
const sections = Object.keys(CATEGORY_LABELS).map((cat) => {
|
||||||
|
const items = byCat[cat];
|
||||||
|
if (!items?.length) return '';
|
||||||
|
return `
|
||||||
|
<div class="servicos-tiles-section">
|
||||||
|
<h4 class="servicos-tiles-section-title">${esc(CATEGORY_LABELS[cat])}</h4>
|
||||||
|
<div class="servicos-tiles-grid">
|
||||||
|
${items.map((s) => serviceTile(s, client)).join('')}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
return `
|
||||||
|
<div class="servicos-client-banner">
|
||||||
|
<strong>${esc(client.displayName)}</strong>
|
||||||
|
<span>${esc(client.subtitle)} · ${esc(client.raw.mail_host || '')}</span>
|
||||||
|
</div>
|
||||||
|
${sections}
|
||||||
|
<p class="servicos-wizard-note">Cada oferta MOSP terá <strong>wizard próprio</strong> (VM112 = só e-mail). Orquestração aqui no Desk VM122 — sem alterar a Hero do portal.</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scopesHtml() {
|
||||||
|
const client = selectedClient();
|
||||||
|
if (!client) {
|
||||||
|
return '<p class="servicos-scope-hint">Escolha um cliente e o serviço <strong>E-mail Tenant</strong> para ver o escopo de purge OPS.</p>';
|
||||||
|
}
|
||||||
|
if (_selectedServiceId !== 'email_tenant') {
|
||||||
|
return `<p class="servicos-scope-hint">Escopo OPS detalhado disponível para <strong>E-mail Tenant</strong>. Serviço actual: ${esc(SERVICE_CATALOG.find((s) => s.id === _selectedServiceId)?.label || _selectedServiceId)}.</p>`;
|
||||||
|
}
|
||||||
|
return OPS_SCOPES.map((s) => `
|
||||||
|
<div class="servicos-scope-item">
|
||||||
|
<strong>${esc(s.label)}</strong>
|
||||||
|
<span>${esc(s.desc)}</span>
|
||||||
|
</div>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function pageHtml() {
|
||||||
|
return `
|
||||||
|
<div class="servicos-page">
|
||||||
|
<header class="servicos-header">
|
||||||
|
<div>
|
||||||
|
<h2 class="servicos-title">Orquestração de Serviços</h2>
|
||||||
|
<p class="servicos-sub">Desk VM122 · Orquestração MOSP</p>
|
||||||
|
<p class="servicos-lead">Clientes e <strong>tenants de oferta</strong> — cada produto com wizard próprio; aqui gere estado OPS e purge de teste.</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-ghost" id="servicos-refresh">Actualizar</button>
|
||||||
|
</header>
|
||||||
|
<div class="servicos-stats-row" id="servicos-stats">${statsHtml()}</div>
|
||||||
|
<div class="servicos-layout">
|
||||||
|
<section class="servicos-panel servicos-panel--clients">
|
||||||
|
<div class="servicos-panel-head">
|
||||||
|
<h3>Clientes <span class="servicos-count" id="servicos-client-count">${filteredClients().length}</span></h3>
|
||||||
|
<input type="search" id="servicos-search" class="servicos-search" placeholder="Pesquisar cliente…" value="${esc(_query)}"/>
|
||||||
|
</div>
|
||||||
|
<div class="servicos-panel-body" id="servicos-client-list">${clientsListHtml()}</div>
|
||||||
|
</section>
|
||||||
|
<section class="servicos-panel servicos-panel--services">
|
||||||
|
<div class="servicos-panel-head"><h3>Tenants de Oferta</h3></div>
|
||||||
|
<div class="servicos-panel-body" id="servicos-services">${servicesGridHtml()}</div>
|
||||||
|
</section>
|
||||||
|
<aside class="servicos-panel servicos-panel--scopes">
|
||||||
|
<div class="servicos-panel-head"><h3>Escopo OPS</h3></div>
|
||||||
|
<div class="servicos-panel-body servicos-scopes" id="servicos-scopes">${scopesHtml()}</div>
|
||||||
|
<p class="servicos-scope-note">Purge (Spec 017): tile E-mail Tenant → modal → senha Root + confirmação do domínio.</p>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshPanels(root) {
|
||||||
|
const list = root.querySelector('#servicos-client-list');
|
||||||
|
const count = root.querySelector('#servicos-client-count');
|
||||||
|
const services = root.querySelector('#servicos-services');
|
||||||
|
const scopes = root.querySelector('#servicos-scopes');
|
||||||
|
const stats = root.querySelector('#servicos-stats');
|
||||||
|
if (list) list.innerHTML = clientsListHtml();
|
||||||
|
if (count) count.textContent = String(filteredClients().length);
|
||||||
|
if (services) services.innerHTML = servicesGridHtml();
|
||||||
|
if (scopes) scopes.innerHTML = scopesHtml();
|
||||||
|
if (stats) stats.innerHTML = statsHtml();
|
||||||
|
bindClientClicks(root);
|
||||||
|
bindServiceClicks(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindPage(root) {
|
||||||
|
root.querySelector('#servicos-refresh')?.addEventListener('click', async () => {
|
||||||
|
root.querySelector('#servicos-services').innerHTML = '<p class="loading">A actualizar…</p>';
|
||||||
|
await loadDomains();
|
||||||
|
refreshPanels(root);
|
||||||
|
});
|
||||||
|
root.querySelector('#servicos-search')?.addEventListener('input', (e) => {
|
||||||
|
_query = e.target.value;
|
||||||
|
refreshPanels(root);
|
||||||
|
});
|
||||||
|
bindClientClicks(root);
|
||||||
|
bindServiceClicks(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindClientClicks(root) {
|
||||||
|
root.querySelectorAll('[data-client-id]').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
_selectedClientId = btn.dataset.clientId;
|
||||||
|
_selectedServiceId = 'email_tenant';
|
||||||
|
refreshPanels(root);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindServiceClicks(root) {
|
||||||
|
root.querySelectorAll('[data-service-id]').forEach((tile) => {
|
||||||
|
tile.addEventListener('click', () => {
|
||||||
|
_selectedServiceId = tile.dataset.serviceId;
|
||||||
|
const client = selectedClient();
|
||||||
|
if (tile.dataset.clickable === '1' && client) {
|
||||||
|
openModal(client.domain);
|
||||||
|
}
|
||||||
|
refreshPanels(root);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTs(iso) {
|
||||||
|
if (!iso) return '—';
|
||||||
|
try {
|
||||||
|
const d = new Date(iso);
|
||||||
|
return d.toLocaleTimeString('pt-PT', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||||
|
} catch {
|
||||||
|
return String(iso);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PURGE_WAIT_STEPS = [
|
||||||
|
...OPS_SCOPES.map((s) => s.label),
|
||||||
|
'Desk — webhook_events',
|
||||||
|
'Desk — tickets',
|
||||||
|
'Desk — audit_domains',
|
||||||
|
'Desk — assist_sessions',
|
||||||
|
'Desk — audit_checks',
|
||||||
|
'Purge concluído',
|
||||||
|
];
|
||||||
|
|
||||||
|
let _purgeElapsedTimer = null;
|
||||||
|
let _purgeLiveSteps = [];
|
||||||
|
|
||||||
|
function upsertPurgeStep(step) {
|
||||||
|
if (!step) return;
|
||||||
|
const runVm112 = _purgeLiveSteps.findIndex(
|
||||||
|
(s) => s.status === 'running' && String(s.label).includes('VM112')
|
||||||
|
);
|
||||||
|
if (runVm112 >= 0 && step.status === 'ok' && String(step.label).includes('VM112')) {
|
||||||
|
_purgeLiveSteps.splice(runVm112, 1);
|
||||||
|
}
|
||||||
|
const sameLabel = _purgeLiveSteps.findIndex((s) => s.label === step.label);
|
||||||
|
if (sameLabel >= 0) {
|
||||||
|
_purgeLiveSteps[sameLabel] = step;
|
||||||
|
} else {
|
||||||
|
_purgeLiveSteps.push(step);
|
||||||
|
}
|
||||||
|
renderPurgeTimeline(_purgeLiveSteps, {
|
||||||
|
running: _purgeLiveSteps.some((s) => s.status === 'running'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPurgeHeartbeat(ev) {
|
||||||
|
const idx = _purgeLiveSteps.findIndex(
|
||||||
|
(s) => s.status === 'running' && String(s.label).includes('VM112')
|
||||||
|
);
|
||||||
|
if (idx < 0) return;
|
||||||
|
_purgeLiveSteps[idx] = {
|
||||||
|
..._purgeLiveSteps[idx],
|
||||||
|
detail: `Em curso na VM112 (${ev.elapsed || 0}s)`,
|
||||||
|
};
|
||||||
|
renderPurgeTimeline(_purgeLiveSteps, { running: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNetworkFetchError(err) {
|
||||||
|
const msg = String(err?.message || err || '');
|
||||||
|
return msg === 'Failed to fetch'
|
||||||
|
|| err?.name === 'AbortError'
|
||||||
|
|| msg.includes('NetworkError')
|
||||||
|
|| msg.includes('network');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recoverPurgeJob(domain, jobId) {
|
||||||
|
const q = domain ? `?domain=${encodeURIComponent(domain)}` : '';
|
||||||
|
return apiFetch(`/v1/vm112/purge/jobs/${encodeURIComponent(jobId)}/recover${q}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: '{}',
|
||||||
|
}, 60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPurgeJobToTimeline(job) {
|
||||||
|
if (!job) return;
|
||||||
|
const steps = Array.isArray(job.timeline) ? job.timeline : [];
|
||||||
|
if (!steps.length) {
|
||||||
|
if (job.status === 'running') {
|
||||||
|
upsertPurgeStep({
|
||||||
|
at: new Date().toISOString(),
|
||||||
|
label: 'Purge em execução',
|
||||||
|
status: 'running',
|
||||||
|
detail: 'A aguardar passos da VM112…',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const step of steps) upsertPurgeStep(step);
|
||||||
|
renderPurgeTimeline(_purgeLiveSteps, { running: job.status === 'running' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showPurgeSuccess(done, domain) {
|
||||||
|
applyPurgeJobToTimeline(done);
|
||||||
|
const deskTotal = Object.values(done?.desk || {}).reduce((a, b) => a + Number(b || 0), 0);
|
||||||
|
upsertPurgeStep({
|
||||||
|
at: new Date().toISOString(),
|
||||||
|
label: 'Purge concluído',
|
||||||
|
status: 'ok',
|
||||||
|
detail: deskTotal ? `Desk: ${deskTotal} registo(s) removido(s)` : (done?.recovered ? 'Recuperação automática' : 'Concluído'),
|
||||||
|
});
|
||||||
|
renderPurgeTimeline(_purgeLiveSteps, { running: false });
|
||||||
|
const msg = document.getElementById('vm112-purge-msg');
|
||||||
|
const btn = document.getElementById('vm112-purge-btn');
|
||||||
|
if (msg) {
|
||||||
|
msg.textContent = `✓ Purge concluído com sucesso.${deskTotal ? ` (${deskTotal} registo(s) Desk)` : ''}`;
|
||||||
|
msg.classList.add('vm112-purge-success');
|
||||||
|
}
|
||||||
|
if (btn) { btn.textContent = 'Concluído ✓'; btn.disabled = true; }
|
||||||
|
_domains = _domains.filter((d) => d.domain !== domain);
|
||||||
|
await loadBillingMap();
|
||||||
|
_clients = buildClients(_domains);
|
||||||
|
if (_selectedClientId === domain) _selectedClientId = null;
|
||||||
|
setTimeout(() => {
|
||||||
|
const el = document.getElementById('overview-home-content');
|
||||||
|
if (el) renderPage(el);
|
||||||
|
closeModal();
|
||||||
|
}, 8000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryRecoverPurge(domain, jobId) {
|
||||||
|
try {
|
||||||
|
const job = await recoverPurgeJob(domain, jobId);
|
||||||
|
applyPurgeJobToTimeline(job);
|
||||||
|
return job?.status === 'done' ? job : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollPurgeJob(domain, confirmDomain, rootPassword, purgeAuthCode = '') {
|
||||||
|
const start = await apiFetch(`/v1/vm112/domains/${encodeURIComponent(domain)}/purge/jobs`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
confirm_domain: confirmDomain,
|
||||||
|
root_password: rootPassword,
|
||||||
|
purge_auth_code: purgeAuthCode || '',
|
||||||
|
}),
|
||||||
|
}, 60000);
|
||||||
|
const jobId = start.job_id;
|
||||||
|
if (!jobId) throw new Error('Job purge não iniciado');
|
||||||
|
_lastPurgeJobId = jobId;
|
||||||
|
let networkErrors = 0;
|
||||||
|
while (true) {
|
||||||
|
let job;
|
||||||
|
try {
|
||||||
|
job = await apiFetch(`/v1/vm112/purge/jobs/${encodeURIComponent(jobId)}`, {}, 60000);
|
||||||
|
networkErrors = 0;
|
||||||
|
} catch (e) {
|
||||||
|
const msg = String(e.message || '');
|
||||||
|
if (msg.includes('não encontrado') || msg.includes('404') || msg === '500' || msg.includes('502') || msg.includes('503')) {
|
||||||
|
const recovered = await tryRecoverPurge(domain, jobId);
|
||||||
|
if (recovered) return recovered;
|
||||||
|
}
|
||||||
|
if (isNetworkFetchError(e)) {
|
||||||
|
networkErrors += 1;
|
||||||
|
upsertPurgeStep({
|
||||||
|
at: new Date().toISOString(),
|
||||||
|
label: 'Ligação ao servidor',
|
||||||
|
status: 'running',
|
||||||
|
detail: `Reconectando… (tentativa ${networkErrors})`,
|
||||||
|
});
|
||||||
|
if (networkErrors >= 2) {
|
||||||
|
const recovered = await tryRecoverPurge(domain, jobId);
|
||||||
|
if (recovered) return recovered;
|
||||||
|
}
|
||||||
|
await sleep(2500);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
applyPurgeJobToTimeline(job);
|
||||||
|
if (job.elapsed_vm112) {
|
||||||
|
const el = document.getElementById('vm112-purge-elapsed');
|
||||||
|
if (el) el.textContent = `${job.elapsed_vm112}s`;
|
||||||
|
}
|
||||||
|
if (job.status === 'done') {
|
||||||
|
return job;
|
||||||
|
}
|
||||||
|
if (job.status === 'error') {
|
||||||
|
throw new Error(job.error || job.timeline?.find((s) => s.status === 'fail')?.detail || 'Purge falhou');
|
||||||
|
}
|
||||||
|
await sleep(2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function consumePurgeStream(domain, confirmDomain, rootPassword) {
|
||||||
|
const res = await fetchWithTimeout(
|
||||||
|
`${API_BASE}/v1/vm112/domains/${encodeURIComponent(domain)}/purge/stream`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: authHeaders({
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'text/event-stream',
|
||||||
|
}),
|
||||||
|
body: JSON.stringify({
|
||||||
|
confirm_domain: confirmDomain,
|
||||||
|
root_password: rootPassword,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
600000
|
||||||
|
);
|
||||||
|
if (res.status === 401) { logout(); throw new Error('sessão expirada'); }
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
const detail = data.detail;
|
||||||
|
throw new Error(typeof detail === 'string' ? detail : `HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
const reader = res.body?.getReader();
|
||||||
|
if (!reader) throw new Error('Stream indisponível no browser');
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
let buf = '';
|
||||||
|
let donePayload = null;
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
buf += decoder.decode(value, { stream: true });
|
||||||
|
const parts = buf.split('\n\n');
|
||||||
|
buf = parts.pop() || '';
|
||||||
|
for (const part of parts) {
|
||||||
|
const line = part.split('\n').find((l) => l.startsWith('data: '));
|
||||||
|
if (!line) continue;
|
||||||
|
let ev;
|
||||||
|
try {
|
||||||
|
ev = JSON.parse(line.slice(6));
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ev.type === 'step') upsertPurgeStep(ev.step);
|
||||||
|
else if (ev.type === 'heartbeat') onPurgeHeartbeat(ev);
|
||||||
|
else if (ev.type === 'error') {
|
||||||
|
upsertPurgeStep(ev.step || { at: new Date().toISOString(), label: 'Purge falhou', status: 'fail', detail: 'erro' });
|
||||||
|
throw new Error(ev.step?.detail || 'Purge falhou');
|
||||||
|
} else if (ev.type === 'done') {
|
||||||
|
upsertPurgeStep(ev.step);
|
||||||
|
donePayload = ev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return donePayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPurgeElapsed() {
|
||||||
|
if (_purgeElapsedTimer) {
|
||||||
|
clearInterval(_purgeElapsedTimer);
|
||||||
|
_purgeElapsedTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPurgeDrawer() {
|
||||||
|
const shell = document.getElementById('vm112-modal-shell');
|
||||||
|
const drawer = document.getElementById('vm112-purge-drawer');
|
||||||
|
if (shell) shell.classList.add('vm112-modal-shell--purge-open');
|
||||||
|
if (drawer) drawer.setAttribute('aria-hidden', 'false');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePurgeDrawer() {
|
||||||
|
stopPurgeElapsed();
|
||||||
|
const shell = document.getElementById('vm112-modal-shell');
|
||||||
|
const drawer = document.getElementById('vm112-purge-drawer');
|
||||||
|
const list = document.getElementById('vm112-purge-timeline');
|
||||||
|
const elapsed = document.getElementById('vm112-purge-elapsed');
|
||||||
|
if (shell) shell.classList.remove('vm112-modal-shell--purge-open');
|
||||||
|
if (drawer) drawer.setAttribute('aria-hidden', 'true');
|
||||||
|
if (list) list.innerHTML = '';
|
||||||
|
if (elapsed) elapsed.textContent = '—';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPurgeTimeline(steps, { running = false } = {}) {
|
||||||
|
const list = document.getElementById('vm112-purge-timeline');
|
||||||
|
if (!list) return;
|
||||||
|
list.innerHTML = (steps || []).map((step, i) => {
|
||||||
|
const status = step.status || 'pending';
|
||||||
|
const isRun = running && status === 'running';
|
||||||
|
return `
|
||||||
|
<li class="vm112-purge-step vm112-purge-step--${esc(status)}${isRun ? ' vm112-purge-step--pulse' : ''}">
|
||||||
|
<span class="vm112-purge-step-time">${esc(formatTs(step.at))}</span>
|
||||||
|
<div class="vm112-purge-step-body">
|
||||||
|
<strong>${esc(step.label)}</strong>
|
||||||
|
${step.detail ? `<span>${esc(step.detail)}</span>` : ''}
|
||||||
|
</div>
|
||||||
|
</li>`;
|
||||||
|
}).join('');
|
||||||
|
list.scrollTop = list.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPurgeElapsed() {
|
||||||
|
const el = document.getElementById('vm112-purge-elapsed');
|
||||||
|
const t0 = Date.now();
|
||||||
|
stopPurgeElapsed();
|
||||||
|
const tick = () => {
|
||||||
|
const sec = Math.floor((Date.now() - t0) / 1000);
|
||||||
|
if (el) el.textContent = `${sec}s`;
|
||||||
|
};
|
||||||
|
tick();
|
||||||
|
_purgeElapsedTimer = setInterval(tick, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initPurgeTimelineRunning() {
|
||||||
|
_purgeLiveSteps = [];
|
||||||
|
_lastPurgeJobId = null;
|
||||||
|
openPurgeDrawer();
|
||||||
|
startPurgeElapsed();
|
||||||
|
upsertPurgeStep({
|
||||||
|
at: new Date().toISOString(),
|
||||||
|
label: 'A iniciar purge',
|
||||||
|
status: 'running',
|
||||||
|
detail: 'A validar credenciais…',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runPurge(domain) {
|
||||||
|
const msg = document.getElementById('vm112-purge-msg');
|
||||||
|
const confirmDomain = document.getElementById('vm112-purge-confirm')?.value?.trim() || '';
|
||||||
|
const rootPassword = document.getElementById('vm112-purge-root-pwd')?.value || '';
|
||||||
|
const purgeAuthCode = document.getElementById('vm112-purge-auth-code')?.value?.trim() || '';
|
||||||
|
const needsAuthCode = Boolean(document.getElementById('vm112-purge-auth-code'));
|
||||||
|
if (!confirmDomain || !rootPassword) {
|
||||||
|
if (msg) msg.textContent = 'Preencha domínio e senha Root.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (needsAuthCode && !purgeAuthCode) {
|
||||||
|
if (msg) msg.textContent = 'Este domínio exige código de autorização — gere em Infra (root).';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!window.confirm(`PURGE definitivo de ${domain}?`)) return;
|
||||||
|
const btn = document.getElementById('vm112-purge-btn');
|
||||||
|
if (btn) { btn.disabled = true; btn.textContent = 'A apagar…'; }
|
||||||
|
if (msg) { msg.textContent = 'A executar purge…'; msg.classList.remove('vm112-purge-success'); }
|
||||||
|
initPurgeTimelineRunning();
|
||||||
|
try {
|
||||||
|
const done = await pollPurgeJob(domain, confirmDomain, rootPassword, purgeAuthCode);
|
||||||
|
stopPurgeElapsed();
|
||||||
|
showPurgeSuccess(done, domain);
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
stopPurgeElapsed();
|
||||||
|
if (isNetworkFetchError(e) && _purgeLiveSteps.some((s) => s.status === 'ok')) {
|
||||||
|
const recovered = _lastPurgeJobId
|
||||||
|
? await tryRecoverPurge(domain, _lastPurgeJobId).catch(() => null)
|
||||||
|
: null;
|
||||||
|
if (recovered) {
|
||||||
|
showPurgeSuccess(recovered, domain);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showPurgeSuccess({ status: 'done', desk: {}, timeline: _purgeLiveSteps, recovered: true }, domain);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!_purgeLiveSteps.length) {
|
||||||
|
const failAt = new Date().toISOString();
|
||||||
|
renderPurgeTimeline([
|
||||||
|
{ at: failAt, label: 'Purge falhou', status: 'fail', detail: e.message || 'erro' },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
const errMsg = isNetworkFetchError(e)
|
||||||
|
? 'Ligação interrompida durante o purge — verifique se o domínio já saiu da lista'
|
||||||
|
: (e.message || 'Purge falhou — verifique se o domínio já foi removido na lista');
|
||||||
|
if (msg) msg.textContent = errMsg;
|
||||||
|
if (btn) { btn.disabled = false; btn.textContent = 'Apagar domínio e todos os dados'; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
closePurgeDrawer();
|
||||||
|
const modal = document.getElementById('vm112-domain-modal');
|
||||||
|
if (!modal) return;
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
modal.setAttribute('aria-hidden', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openModal(domain) {
|
||||||
|
const modal = document.getElementById('vm112-domain-modal');
|
||||||
|
const body = document.getElementById('vm112-domain-modal-body');
|
||||||
|
const title = document.getElementById('vm112-domain-modal-title');
|
||||||
|
const sub = document.getElementById('vm112-domain-modal-sub');
|
||||||
|
if (!modal || !body) return;
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
modal.setAttribute('aria-hidden', 'false');
|
||||||
|
title.textContent = domain;
|
||||||
|
sub.textContent = 'E-mail Tenant · a carregar…';
|
||||||
|
body.innerHTML = '<p class="loading">A carregar detalhe VM112…</p>';
|
||||||
|
loadModal(domain);
|
||||||
|
modal.querySelectorAll('[data-close-vm112-modal]').forEach((el) => {
|
||||||
|
el.onclick = closeModal;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadModal(domain) {
|
||||||
|
const body = document.getElementById('vm112-domain-modal-body');
|
||||||
|
const sub = document.getElementById('vm112-domain-modal-sub');
|
||||||
|
try {
|
||||||
|
const d = await apiFetch(`/v1/vm112/domains/${encodeURIComponent(domain)}`, {}, 180000);
|
||||||
|
const accN = (d.accounts || []).length;
|
||||||
|
sub.textContent = `E-mail Tenant · ${accN} conta(s) · ${d.mail_host || ''}`;
|
||||||
|
const steps = (d.infra_status?.steps || [])
|
||||||
|
.map((s) => `<li class="${s.ok ? 'ok' : 'fail'}"><strong>${esc(s.label)}</strong> — ${esc(s.message)}</li>`)
|
||||||
|
.join('');
|
||||||
|
const accounts = (d.accounts || [])
|
||||||
|
.map((a) => `<li>${esc(a)}</li>`).join('') || '<li class="muted">Nenhuma</li>';
|
||||||
|
const cf = d.cloudflare_zone;
|
||||||
|
const cfTxt = cf?.name ? `Zona ${cf.name} (${cf.status || '—'})` : 'Sem zona Cloudflare Ibytera';
|
||||||
|
const extraAuth = Boolean(d.purge_extra_auth_required);
|
||||||
|
const authCodeField = extraAuth
|
||||||
|
? `
|
||||||
|
<p class="vm112-purge-warn">Domínio protegido — exige <strong>código de autorização</strong> gerado em Infra (root).</p>
|
||||||
|
<label>Código autorização purge</label>
|
||||||
|
<input type="text" id="vm112-purge-auth-code" class="vm112-purge-input" placeholder="XXXX-XXXX" autocomplete="off" spellcheck="false"/>
|
||||||
|
`
|
||||||
|
: '';
|
||||||
|
body.innerHTML = `
|
||||||
|
<div class="modal-section">
|
||||||
|
<h4>Serviço: E-mail Tenant</h4>
|
||||||
|
<p><strong>Cliente / domínio:</strong> ${esc(domain)}</p>
|
||||||
|
<p><strong>Admin portal:</strong> ${esc(d.portal_admin_email || '—')}</p>
|
||||||
|
<p><strong>Carbonio:</strong> ${d.carbonio_exists ? 'ativo' : 'ausente'}</p>
|
||||||
|
<p><strong>Pasta site:</strong> ${d.site_folder_exists ? 'sim' : 'não'}</p>
|
||||||
|
<p><strong>Cloudflare:</strong> ${esc(cfTxt)}</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-section">
|
||||||
|
<h4>Infraestrutura VM112</h4>
|
||||||
|
<ul class="vm112-infra-steps">${steps || '<li>—</li>'}</ul>
|
||||||
|
</div>
|
||||||
|
<div class="modal-section">
|
||||||
|
<h4>Contas Carbonio</h4>
|
||||||
|
<ul>${accounts}</ul>
|
||||||
|
</div>
|
||||||
|
<div class="modal-section vm112-purge-zone">
|
||||||
|
<h4>Purge completo (testes — Spec 017)</h4>
|
||||||
|
<p class="vm112-purge-warn">Remove Carbonio, site, portal users, Cloudflare, Traefik e registos Desk. <strong>Irreversível.</strong> Requer senha Root.</p>
|
||||||
|
<label>Confirmar domínio</label>
|
||||||
|
<input type="text" id="vm112-purge-confirm" class="vm112-purge-input" placeholder="${esc(domain)}" autocomplete="off"/>
|
||||||
|
<label>Senha Root</label>
|
||||||
|
<input type="password" id="vm112-purge-root-pwd" class="vm112-purge-input" autocomplete="current-password"/>
|
||||||
|
${authCodeField}
|
||||||
|
<button type="button" class="btn btn-danger" id="vm112-purge-btn">Apagar domínio e todos os dados</button>
|
||||||
|
<p id="vm112-purge-msg" class="ticket-meta"></p>
|
||||||
|
</div>`;
|
||||||
|
body.querySelector('#vm112-purge-btn')?.addEventListener('click', () => runPurge(domain));
|
||||||
|
} catch (e) {
|
||||||
|
body.innerHTML = `<p class="loading">Erro: ${esc(e.message)}</p>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPollStatus(root, message, isError = false) {
|
||||||
|
if (!root) return;
|
||||||
|
let el = root.querySelector('#servicos-poll-status');
|
||||||
|
if (!message) {
|
||||||
|
el?.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!el) {
|
||||||
|
el = document.createElement('p');
|
||||||
|
el.id = 'servicos-poll-status';
|
||||||
|
el.className = 'servicos-poll-status';
|
||||||
|
root.querySelector('.servicos-header')?.appendChild(el);
|
||||||
|
}
|
||||||
|
el.className = `servicos-poll-status${isError ? ' servicos-poll-status--err' : ''}`;
|
||||||
|
el.textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderPage(container, options = {}) {
|
||||||
|
const { poll = false } = options;
|
||||||
|
if (!container) return;
|
||||||
|
if (!canAccess()) {
|
||||||
|
container.innerHTML = '<p class="loading">Sem permissão — perfil Admin (super_admin ou ops_lead) necessário.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hasPage = Boolean(container.querySelector('.servicos-page'));
|
||||||
|
if (poll && hasPage) {
|
||||||
|
try {
|
||||||
|
await loadDomains();
|
||||||
|
refreshPanels(container);
|
||||||
|
setPollStatus(container, '');
|
||||||
|
} catch (e) {
|
||||||
|
setPollStatus(container, `Actualização falhou: ${e.message}`, true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!hasPage) {
|
||||||
|
container.innerHTML = '<p class="loading">A carregar clientes e serviços VM112…</p>';
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await loadDomains();
|
||||||
|
if (_clients.length && !_selectedClientId) {
|
||||||
|
_selectedClientId = _clients[0].id;
|
||||||
|
}
|
||||||
|
container.innerHTML = pageHtml();
|
||||||
|
bindPage(container);
|
||||||
|
setPollStatus(container, '');
|
||||||
|
} catch (e) {
|
||||||
|
if (hasPage) {
|
||||||
|
setPollStatus(container, `Erro ao carregar VM112: ${e.message}`, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
container.innerHTML = `<div class="servicos-page"><p class="servicos-empty">Erro ao carregar VM112: ${esc(e.message)}</p><button type="button" class="btn" id="servicos-retry">Tentar de novo</button></div>`;
|
||||||
|
container.querySelector('#servicos-retry')?.addEventListener('click', () => renderPage(container));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { renderPage, loadDomains, openModal, canAccess, SERVICE_CATALOG };
|
||||||
|
})();
|
||||||
|
|
||||||
|
window.DeskServices = DeskServices;
|
||||||
|
window.DeskAccounts = DeskServices;
|
||||||
4814
projects/ops-desk/frontend/staging-snapshot/stg-styles.css
Normal file
4814
projects/ops-desk/frontend/staging-snapshot/stg-styles.css
Normal file
File diff suppressed because it is too large
Load diff
128
projects/ops-desk/frontend/staging-snapshot/stg-topnav.js
Normal file
128
projects/ops-desk/frontend/staging-snapshot/stg-topnav.js
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
/** Desk chrome nav — tabs planas por zona + sidebar contextual (v0.11). */
|
||||||
|
|
||||||
|
const TOPNAV_VIEW_GROUP = {
|
||||||
|
'agentic-ops': 'operacoes',
|
||||||
|
dashboard: 'operacoes',
|
||||||
|
tickets: 'operacoes',
|
||||||
|
events: 'operacoes',
|
||||||
|
'overview-home': 'comercial',
|
||||||
|
leads: 'comercial',
|
||||||
|
infra: 'infra',
|
||||||
|
infra2: 'infra',
|
||||||
|
'email-migration': 'infra',
|
||||||
|
overview: 'sistema',
|
||||||
|
account: 'sistema',
|
||||||
|
messages: 'sistema',
|
||||||
|
admin: 'sistema',
|
||||||
|
'access-matrix': 'sistema',
|
||||||
|
modules: 'sistema',
|
||||||
|
};
|
||||||
|
|
||||||
|
const CONTEXT_TOOLBARS = {
|
||||||
|
tickets: { title: 'Tickets', host: '#view-tickets', label: 'Filas' },
|
||||||
|
events: { title: 'Eventos', host: '#view-events', label: 'Vistas' },
|
||||||
|
};
|
||||||
|
|
||||||
|
function initTopnavDropdowns() {
|
||||||
|
/* v0.11 — tabs planas; sem dropdowns */
|
||||||
|
}
|
||||||
|
|
||||||
|
function parkContextToolbars() {
|
||||||
|
Object.values(CONTEXT_TOOLBARS).forEach(({ host }) => {
|
||||||
|
const view = document.querySelector(host);
|
||||||
|
const toolbar = view?.querySelector('.toolbar');
|
||||||
|
if (toolbar && view && toolbar.parentElement !== view) {
|
||||||
|
view.insertBefore(toolbar, view.firstChild);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const matrixRoleNav = document.getElementById('context-matrix-roles');
|
||||||
|
if (matrixRoleNav && matrixRoleNav.dataset.parkedFrom) {
|
||||||
|
const slot = document.querySelector(matrixRoleNav.dataset.parkedFrom);
|
||||||
|
if (slot) slot.appendChild(matrixRoleNav);
|
||||||
|
delete matrixRoleNav.dataset.parkedFrom;
|
||||||
|
}
|
||||||
|
const adminHost = document.getElementById('context-admin-tabs');
|
||||||
|
if (adminHost) adminHost.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTopnavActive(view) {
|
||||||
|
document.querySelectorAll('[data-desk-nav]').forEach((btn) => {
|
||||||
|
const active = btn.dataset.view === view;
|
||||||
|
btn.classList.toggle('active', active);
|
||||||
|
if (active) btn.setAttribute('aria-current', 'page');
|
||||||
|
else btn.removeAttribute('aria-current');
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.nav-zone[data-topnav-group]').forEach((zone) => {
|
||||||
|
zone.classList.toggle('active', TOPNAV_VIEW_GROUP[view] === zone.dataset.topnavGroup);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountMatrixRoleSidebar() {
|
||||||
|
const roleNav = document.querySelector('#access-matrix-content .am-role-list');
|
||||||
|
if (!roleNav) return false;
|
||||||
|
const nav = document.getElementById('context-nav');
|
||||||
|
if (!nav) return false;
|
||||||
|
roleNav.dataset.parkedFrom = '#access-matrix-content .am-layout';
|
||||||
|
nav.appendChild(roleNav);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateContextSidebar(view) {
|
||||||
|
const shell = document.querySelector('.shell');
|
||||||
|
const sidebar = document.getElementById('context-sidebar');
|
||||||
|
const nav = document.getElementById('context-nav');
|
||||||
|
const titleEl = document.getElementById('context-sidebar-title');
|
||||||
|
const labelEl = document.getElementById('context-sidebar-label');
|
||||||
|
if (!shell || !sidebar || !nav) return;
|
||||||
|
|
||||||
|
parkContextToolbars();
|
||||||
|
nav.innerHTML = '';
|
||||||
|
|
||||||
|
if (view === 'admin' && typeof renderAdminTabs === 'function') {
|
||||||
|
sidebar.hidden = false;
|
||||||
|
shell.classList.remove('shell--no-context');
|
||||||
|
if (titleEl) titleEl.textContent = 'Administradores';
|
||||||
|
if (labelEl) labelEl.textContent = 'Secções';
|
||||||
|
nav.innerHTML = `<div id="context-admin-tabs">${renderAdminTabs()}</div>`;
|
||||||
|
if (typeof bindAdminTabs === 'function') bindAdminTabs();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view === 'access-matrix') {
|
||||||
|
sidebar.hidden = false;
|
||||||
|
shell.classList.remove('shell--no-context');
|
||||||
|
if (titleEl) titleEl.textContent = 'Matriz de Acesso';
|
||||||
|
if (labelEl) labelEl.textContent = 'Funções';
|
||||||
|
requestAnimationFrame(() => mountMatrixRoleSidebar());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cfg = CONTEXT_TOOLBARS[view];
|
||||||
|
if (!cfg) {
|
||||||
|
sidebar.hidden = true;
|
||||||
|
shell.classList.add('shell--no-context');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewEl = document.querySelector(cfg.host);
|
||||||
|
const toolbar = viewEl?.querySelector('.toolbar');
|
||||||
|
if (!toolbar) {
|
||||||
|
sidebar.hidden = true;
|
||||||
|
shell.classList.add('shell--no-context');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sidebar.hidden = false;
|
||||||
|
shell.classList.remove('shell--no-context');
|
||||||
|
if (titleEl) titleEl.textContent = cfg.title;
|
||||||
|
if (labelEl) labelEl.textContent = cfg.label;
|
||||||
|
toolbar.classList.add('context-toolbar');
|
||||||
|
nav.appendChild(toolbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.DeskTopnav = {
|
||||||
|
initTopnavDropdowns,
|
||||||
|
updateTopnavActive,
|
||||||
|
updateContextSidebar,
|
||||||
|
remountMatrixRoles: mountMatrixRoleSidebar,
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue