diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000000..41d2ad4e6d
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,81 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+---
+
+## [Unreleased] — Root Intelligence UI
+
+> **Codename:** Root Intelligence
+> **Base:** OpenPrinting CUPS 2.5b1
+> **Scope:** Frontend / Web Administration Interface only
+> **Backend compatibility:** 100% preserved — no CGI, IPP or build system logic was altered.
+
+### Added
+
+#### Design System (`doc/cups.css`)
+- Dark glassmorphism theme with `backdrop-filter: blur(14px)` and semi-transparent panels
+- Cohesive color palette: deep navy `#0d1117` background + sage green `#a8c686` accent
+- CSS custom properties (`--ri-*`) for consistent, easy theming across all components
+- Smooth transitions (`0.3s cubic-bezier(0.4, 0, 0.2, 1)`) on all interactive elements
+- Subtle glow borders (`box-shadow: 0 0 16px rgba(168, 198, 134, 0.22)`) on focus/hover
+- Animated status badges with pulse effect for printer states (idle, processing, stopped)
+- Full responsive layout covering 360px (mobile) → 768px (tablet) → 1440px+ (desktop)
+
+#### JavaScript Interactive Layer (`doc/cups.js` — **new file**)
+- **Zero external dependencies** — runs fully offline, suitable for air-gapped Linux servers
+- **User Mode Toggle** (Beginner ↔ Advanced) persisted via `localStorage`
+ - Beginner mode: hides advanced panels, shows contextual help tooltips
+ - Advanced mode: reveals telemetry bar, raw command shortcuts, diagnostic info
+- **Onboarding Checklist** with animated progress bar for first-time users
+ - 3 guided steps: verify daemon, add printer, print test page
+ - State persisted in `localStorage`; dismissable permanently
+- **Guided Tour Modal** — interactive 3-step walkthrough explaining:
+ 1. CUPS architecture and the role of the web interface
+ 2. Local diagnostics without leaving the browser
+ 3. Power-user shortcuts and command reference
+- **Command Palette** (`Ctrl+K` / `⌘K`) — Raycast/Spotlight-style launcher
+ - Real-time fuzzy search across: quick actions, print queues, terminal commands
+ - Keyboard navigation with `↑` `↓`, `Enter` to execute, `Esc` to close
+ - Copy-to-clipboard for shell commands (`lpstat -p -d`, `cupsenable`, `cancel -a`)
+ - Mouse-friendly with hover highlight and click activation
+
+#### Template Enhancements (`templates/` + `templates/pt_BR/`)
+- `header.tmpl`: Command Palette trigger button + Mode Switch toggle in navigation bar
+- `home.tmpl`: Onboarding banner + Advanced telemetry status bar
+- `printers.tmpl` / `printer.tmpl`: Animated printer cards with status glow indicators
+- `jobs.tmpl` / `jobs-header.tmpl`: Modernized job list with inline action buttons
+- `admin.tmpl`: Redesigned admin panel with section cards and descriptive labels
+- `classes.tmpl` / `class.tmpl`: Consistent dark card treatment for printer classes
+- `error.tmpl`: Friendly error page with contextual recovery suggestions
+- `search.tmpl`: Styled search results with highlighted matches
+- `trailer.tmpl`: Updated footer with version reference and keyboard shortcut hint
+- All templates: accessible focus rings, semantic HTML improvements, ARIA labels
+
+#### Developer Tooling (`tools/` — **new directory**)
+- `cups_preview_server.py`: Local Python HTTP server simulating CUPS CGI routing
+- `cups_template.py`: Lightweight template engine rendering `.tmpl` files offline
+- `capture_step4.py` / `capture_step5.py`: Automated Playwright screenshot scripts
+- `capture_all_screens.py`: Batch visual regression capture (Desktop/Tablet/Mobile)
+
+### Changed
+- `doc/Makefile`: Added `cups.js` to `WEBPAGES` install target
+- `doc/cups.css`: Extended with Section 11 (Root Intelligence additions), leaving original sections intact
+- `doc/cups-printable.css`: Minor alignment fixes for print stylesheet
+
+### Not Changed (Intentionally)
+- All CGI backend source files (`scheduler/`, `cgi-bin/`, `cups/`)
+- Template variable interpolation syntax (`{variable}`, `{#array=0?then:else}`)
+- Form `OP` parameter routing and `org.cups.sid` CSRF token handling
+- `configure.ac`, `Makefile.in`, autoconf build pipeline
+- IPP protocol handling and PPD driver logic
+
+---
+
+## Previous Releases
+
+For the upstream OpenPrinting CUPS changelog, see:
+https://github.com/OpenPrinting/cups/blob/master/CHANGES.md
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f20e3090c3..0f6de09e81 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,10 +1,256 @@
-Contributing to CUPS
-====================
+# Contributing to CUPS — Root Intelligence UI
-CUPS is developed by OpenPrinting and distributed as open source software under
-the Apache License, Version 2.0 with exceptions to allow linking to GPL2/LGPL2
-code.
+Thank you for your interest in improving the **Root Intelligence** frontend for
+OpenPrinting CUPS! This document describes conventions, file layout, and the
+development workflow used in this fork.
-Contributions should be submitted either as pull requests or as attachments
-(unified diffs) to bug reports on the OpenPrinting CUPS GitHub project at
-.
+> **Scope of this fork:** Only the web administration frontend is modified here.
+> For contributions to the CUPS printing backend, daemon, PPD handling or IPP
+> protocol — please contribute directly to
+> [OpenPrinting/cups](https://github.com/OpenPrinting/cups).
+
+---
+
+## Table of Contents
+
+1. [Philosophy](#philosophy)
+2. [Getting Started](#getting-started)
+3. [Project Structure](#project-structure)
+4. [Frontend Architecture](#frontend-architecture)
+5. [Template Conventions](#template-conventions)
+6. [CSS Conventions](#css-conventions)
+7. [JavaScript Conventions](#javascript-conventions)
+8. [Testing Locally](#testing-locally)
+9. [Submitting a Pull Request](#submitting-a-pull-request)
+10. [Code of Conduct](#code-of-conduct)
+
+---
+
+## Philosophy
+
+> *"A clareza do terminal. A inteligência do contexto. O controle continua seu."*
+> *("The clarity of the terminal. The intelligence of context. The control remains yours.")*
+
+This fork values:
+
+- **Autonomy** — The interface works fully offline; no CDN, no external fonts required.
+- **Transparency** — Every action surfaced in the UI maps to a real CUPS/IPP operation.
+- **Efficiency** — Power users shouldn't need more than 2 clicks or 1 keyboard shortcut.
+- **Composability** — UI components are independent; removing one doesn't break others.
+- **Beginner-friendliness** — Beginner Mode, Onboarding Checklist, and Tour Modal lower the barrier without condescending to experienced users.
+
+---
+
+## Getting Started
+
+### Prerequisites
+
+- Git
+- Python 3.8+ (for the local preview server)
+- A modern browser (Chrome/Firefox/Edge)
+- Optional: Playwright for automated screenshots
+
+### Clone and preview
+
+```bash
+git clone https://github.com//cups.git
+cd cups
+
+# Start the local preview server (no CUPS daemon needed)
+python tools/cups_preview_server.py
+
+# Open in browser
+open http://localhost:8631
+```
+
+The preview server renders all `.tmpl` files with realistic fixture data so you
+can iterate on the UI without compiling CUPS or running the daemon.
+
+---
+
+## Project Structure
+
+```
+cups/
+├── doc/
+│ ├── cups.css # Main stylesheet (see CSS Conventions)
+│ ├── cups.js # JavaScript layer (see JavaScript Conventions)
+│ ├── cups-printable.css # Print stylesheet
+│ └── Makefile # Install targets (cups.js added here)
+│
+├── templates/ # English templates
+│ ├── header.tmpl # Global nav: mode toggle + command palette button
+│ ├── home.tmpl # Dashboard: onboarding banner + telemetry bar
+│ ├── printers.tmpl # Printer list
+│ ├── printer.tmpl # Printer detail
+│ ├── jobs.tmpl # Job list
+│ ├── admin.tmpl # Administration panel
+│ └── ...
+│
+├── templates/pt_BR/ # Brazilian Portuguese translations (mirrors templates/)
+│
+└── tools/ # Developer tooling (not shipped with CUPS)
+ ├── cups_preview_server.py
+ ├── cups_template.py
+ ├── capture_step4.py
+ ├── capture_step5.py
+ └── capture_all_screens.py
+```
+
+---
+
+## Frontend Architecture
+
+The UI layer is a **progressive enhancement** stack:
+
+```
+HTML (templates/*.tmpl) <- always works, even without JS
+ └── CSS (doc/cups.css) <- visual design; mode-visibility rules
+ └── JS (doc/cups.js) <- interactive features; mounts safely if DOM ready
+```
+
+**Key principle:** If `cups.js` fails to load or JS is disabled, the page remains
+fully functional — the Command Palette trigger button simply won't respond, and
+Mode Toggle will default to Beginner styling via CSS alone.
+
+### Component inventory (cups.js)
+
+| Export / init function | Description |
+|---|---|
+| `initModeToggle()` | Beginner vs Advanced switch, `localStorage` persistence |
+| `initOnboarding()` | Checklist banner with animated progress bar |
+| `initTour()` | 3-step guided tour modal |
+| `initCommandPalette()` | Ctrl+K launcher with fuzzy search |
+| `DOMContentLoaded` listener | Mounts all components after DOM is ready |
+
+---
+
+## Template Conventions
+
+CUPS templates use a custom variable syntax — **do not break it**:
+
+| Syntax | Meaning |
+|---|---|
+| `{variable}` | Interpolated string value |
+| `{?condition}` ... `{/condition}` | Conditional block |
+| `{#array=0?then:else}` | Array iteration |
+| `{[localized string]}` | Localized string lookup |
+
+**Rules:**
+1. Never add Handlebars / Jinja2 / Mustache syntax — it breaks the CGI renderer.
+2. Preserve ` ` on every form.
+3. Keep `OP` parameter routing intact: ` `.
+4. Mirror every change in `templates/` to `templates/pt_BR/` (translate user-visible strings).
+
+---
+
+## CSS Conventions
+
+All Root Intelligence additions live in **Section 11** of `cups.css`, clearly delimited:
+
+```css
+/* ============================================================
+ §11 — ROOT INTELLIGENCE (UI OVERHAUL)
+ ============================================================ */
+```
+
+**Rules:**
+- Use CSS custom properties defined in `:root` under §11 — never hard-code colors.
+- Variable naming: `--ri--` e.g. `--ri-accent`, `--ri-surface-glass`.
+- Prefix all new class names with `.ri-` to avoid collisions with legacy CUPS styles.
+- Transitions must respect `prefers-reduced-motion`.
+- Do not modify Sections 1–10 unless fixing a collision with legacy rules.
+
+### Color palette reference
+
+| Token | Value | Usage |
+|---|---|---|
+| `--ri-bg` | `#0d1117` | Page background |
+| `--ri-surface` | `rgba(22,27,34,0.85)` | Card/panel backgrounds |
+| `--ri-accent` | `#a8c686` | Primary accent (sage green) |
+| `--ri-accent-glow` | `rgba(168,198,134,0.22)` | Glow shadows |
+| `--ri-text` | `#e6edf3` | Primary text |
+| `--ri-text-muted` | `#8b949e` | Secondary/muted text |
+| `--ri-border` | `rgba(48,54,61,0.8)` | Borders |
+| `--ri-error` | `#f85149` | Error states |
+| `--ri-warning` | `#d29922` | Warning states |
+| `--ri-success` | `#3fb950` | Success states |
+
+---
+
+## JavaScript Conventions
+
+- **No external dependencies.** Do not import lodash, jQuery, Alpine.js, etc.
+- **No build step required.** `cups.js` is vanilla ES2017.
+- **Guard every DOM query:**
+
+```js
+const el = document.getElementById('ri-cmd-palette');
+if (!el) return; // graceful no-op if template doesn't include this element
+```
+
+- **Use `localStorage` for persistence** — not cookies, not sessionStorage.
+- **Key naming:** `cups.ri.` e.g. `cups.ri.mode`, `cups.ri.onboarding.done`.
+- Add JSDoc comments to every exported function.
+
+---
+
+## Testing Locally
+
+### Visual regression (automated)
+
+```bash
+# Install Playwright (one-time)
+pip install playwright
+playwright install chromium
+
+# Run screenshot capture
+python tools/capture_step5.py
+```
+
+### Manual checklist before opening a PR
+
+- [ ] Preview server renders all pages without errors
+- [ ] Beginner Mode hides advanced-only elements; Advanced Mode shows them
+- [ ] Command Palette opens with `Ctrl+K` and closes with `Esc`
+- [ ] Onboarding banner appears on first load and persists via localStorage
+- [ ] Tour Modal navigates all 3 steps and closes cleanly
+- [ ] No JavaScript errors in browser console
+- [ ] `pt_BR` templates render correctly
+- [ ] Mobile layout (360px) has no horizontal overflow
+- [ ] `prefers-reduced-motion` disables all animations
+
+---
+
+## Submitting a Pull Request
+
+1. **Fork** this repository and create a branch: `feat/your-feature-name`
+2. **Follow conventions** described in this document
+3. **Run the manual checklist** above
+4. **Open a PR** with:
+ - A clear title: `feat(ui): ...` / `fix(css): ...` / `docs: ...`
+ - A short description of *what* changed and *why*
+ - Screenshots or screen recording if your change is visual
+
+### Commit message format
+
+```
+():
+```
+
+Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
+Scopes: `ui`, `css`, `js`, `template`, `tools`, `build`
+
+---
+
+## Code of Conduct
+
+This project follows the
+[Contributor Covenant v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
+
+Be respectful. Be curious. Build tools that empower people.
+
+---
+
+*Root Intelligence is a community concept, not an official OpenPrinting product.
+For upstream CUPS issues, please visit https://github.com/OpenPrinting/cups/issues.*
diff --git a/PROMPT_CUPS_LINUX_RAIZ_IA.txt b/PROMPT_CUPS_LINUX_RAIZ_IA.txt
new file mode 100644
index 0000000000..172735ab9f
--- /dev/null
+++ b/PROMPT_CUPS_LINUX_RAIZ_IA.txt
@@ -0,0 +1,229 @@
+CUPS / ROOT INTELLIGENCE
+Prompt de implementação — Linux raiz, minimalismo e inteligência local
+
+Você é responsável pelo design e pela implementação de uma evolução da interface
+web deste repositório OpenPrinting CUPS. Crie uma experiência de administração de
+impressão contemporânea, precisa e sofisticada, com referências à cultura Unix.
+Implemente em incrementos executáveis e verificados; não entregue apenas conceitos.
+
+IDEIA CENTRAL
+
+“A clareza do terminal. A inteligência do contexto. O controle continua seu.”
+
+Trate “Root Intelligence” como nome conceitual da proposta, não como indicação de
+executar processos como root nem como marca oficial do OpenPrinting.
+
+A direção escolhida valoriza características presentes em muitas ferramentas da
+cultura Linux: autonomia, transparência, eficiência, composição e controle local.
+Não presuma que toda a comunidade compartilha uma preferência estética única.
+O resultado deve parecer uma ferramenta de trabalho mantida por pessoas que
+entendem sistemas, com acabamento visual cuidadoso e personalidade própria.
+
+1. COMECE PELO PROJETO REAL
+
+Leia as instruções locais e inspecione a arquitetura antes de alterar arquivos.
+Este checkout contém CUPS com código C, interface CGI e templates próprios.
+Pontos de partida identificados:
+
+- doc/cups.css: estilos compartilhados da interface.
+- doc/cups-printable.css: estilos de impressão.
+- templates/header.tmpl: cabeçalho e navegação padrão.
+- templates/pt_BR/header.tmpl: cabeçalho em português brasileiro.
+- templates/admin.tmpl: administração.
+- templates/printers.tmpl e templates/printer.tmpl: impressoras.
+- templates/jobs.tmpl: trabalhos e ações.
+- templates/pt_BR/: traduções dos templates.
+- cgi-bin/: controladores da interface web.
+- doc/Makefile: instalação dos recursos web.
+- doc/help/ e man/: documentação do projeto.
+
+Verifique também os demais templates, idiomas, páginas iniciais e caminhos de
+instalação. Não presuma a existência de endpoints JSON ou de um frontend React.
+Priorize HTML semântico, CSS e JavaScript progressivo quando necessário.
+Qualquer novo recurso estático precisa entrar no processo de instalação.
+
+Preserve os textos, dados, rotas, traduções, regras e funcionalidades existentes.
+Preserve a sintaxe dos templates, condicionais, formulários, métodos, campos OP,
+identificadores e tokens org.cups.sid. Mantenha autenticação, autorização e
+proteções de sessão. Melhorias de texto devem conservar o significado operacional.
+
+2. DIREÇÃO VISUAL: UNIX CONTEMPORÂNEO
+
+Referências conceituais: organização de man pages, precisão de utilitários como
+htop, divisões de painéis do tmux e simplicidade das interfaces de terminal.
+Traduza essas referências em uma interface gráfica original e acessível.
+
+Use fundo grafite quase preto, superfícies discretamente elevadas, linhas finas,
+margens generosas e alinhamento rigoroso. Evite uma sequência de cartões enormes.
+Priorize tabelas, listas de eventos, divisores e painéis compactos bem organizados.
+
+Paleta inicial, sujeita à verificação de contraste:
+ fundo principal #101211
+ superfície #181C19
+ superfície elevada #202622
+ divisores #343D36
+ texto principal #EDF2EC
+ texto secundário #B0BCAF
+ destaque verde sálvia #A8C686
+ atenção âmbar #E4BC75
+ erro coral #EF9990
+ informação azul #9FBFCB
+
+Use verde como acento pontual para seleção, foco e ações principais.
+Estados precisam de texto e/ou ícone além da cor.
+Defina tokens CSS para cores, espaçamento, tipografia e bordas.
+Use escala de espaçamento de 4, 8, 12, 16, 24, 32 e 48 px.
+Bordas de 1 px e raios contidos, aproximadamente 4 a 8 px.
+
+Use fonte de sistema legível nos textos e ui-monospace nos nomes de filas,
+identificadores, horários, comandos, rótulos técnicos e valores tabulares.
+Não faça a interface depender de fontes remotas; se incorporar fontes, hospede-as
+localmente e respeite suas licenças. Inspecione o @import remoto do CSS atual.
+Texto principal entre 14 e 16 px; títulos claros e sem proporções de landing page.
+
+Referências raiz em pequenos detalhes:
+- Um marcador “>_” discreto junto à identidade, preservando os créditos do CUPS.
+- Cabeçalhos de seção compactos, com referências como /printers e /jobs.
+- Horários alinhados e números com largura tabular.
+- Um detalhe ASCII curto em estados vazios, acompanhado de explicação útil.
+- Blocos de comando selecionáveis, com copiar e descrição do efeito.
+
+Evite chuva Matrix, neon intenso, scanlines, glitch, fundo animado, cursores
+piscando sem função, vidro translúcido e gradientes decorativos dominantes.
+A sofisticação deve vir de proporção, contraste, ritmo e qualidade da informação.
+
+3. ESTRUTURA DA EXPERIÊNCIA
+
+Mantenha acesso evidente a Início, Impressoras, Trabalhos, Classes, Administração
+e Ajuda. Preserve busca, identificação de sessão e login/logout quando disponíveis.
+Prefira uma barra superior compacta; adapte a navegação a telas pequenas sem
+ocultar destinos ou sobrepor o conteúdo.
+
+Início:
+- Apresente identidade do servidor e estado observado, se disponíveis.
+- Resuma filas e trabalhos com dados reais e horário da última atualização.
+- Dê prioridade a situações que precisam de atenção.
+- Mostre “Não disponível” quando não houver uma fonte implementada.
+- Não invente uptime, economia, quantidade de impressões ou saúde do servidor.
+
+Impressoras:
+- Tabela com fila, descrição, localização, modelo e estado.
+- Preserve todos os campos existentes e suas ações.
+- Busca e filtros devem funcionar sobre dados reais, com estado vazio explícito.
+- No detalhe, organize propriedades, trabalhos e ações administrativas.
+- Não transforme um estado “ociosa” em garantia de conectividade física.
+
+Trabalhos:
+- Preserve identificação, usuário, tamanho, páginas, estado e ações permitidas.
+- Dê distinção clara a pendente, retido, processando, parado, cancelado,
+ abortado e concluído, conforme os dados existentes.
+- Mantenha as permissões e condições atuais de cancelar, mover, liberar e reimprimir.
+- Use confirmações proporcionais para ações destrutivas, com o alvo explícito.
+
+Administração e ajuda:
+- Agrupe configurações por finalidade e explique consequências relevantes.
+- Mantenha recursos avançados acessíveis e documentados.
+- Use divulgação progressiva para reduzir ruído sem eliminar funcionalidades.
+- Preserve a documentação local e a utilização sem acesso à internet.
+
+4. IA COM UTILIDADE OPERACIONAL
+
+Crie uma área contextual “Assistência de diagnóstico”, acessível a partir de uma
+fila ou ocorrência. Seu propósito é ajudar a entender evidências do sistema.
+O administrador deve conseguir trabalhar normalmente com a IA desligada.
+
+Casos de uso prioritários:
+- Explicar mensagens e estados de impressão em português claro.
+- Resumir eventos relevantes de uma fila dentro de uma janela de tempo.
+- Sugerir hipóteses e verificações com base nos dados realmente consultados.
+- Explicar comandos propostos antes que alguém decida utilizá-los.
+- Preparar um resumo técnico copiável para suporte.
+
+Cada diagnóstico deve separar:
+ Observado: estado ou mensagem real, origem e horário.
+ Hipótese: interpretação possível, explicitamente identificada como hipótese.
+ Verificação: próximo passo e resultado esperado para confirmar ou refutar.
+ Ação sugerida: efeito previsto e permissões necessárias, quando aplicável.
+
+Exemplo de tom, sem apresentar isto como dado real do servidor:
+ Observado: a fila está pausada.
+ Hipótese: a pausa pode ter sido manual ou associada a um erro anterior.
+ Verificação: consultar a mensagem da fila e os eventos correspondentes.
+
+Não atribua uma causa com certeza quando os dados só indicam um sintoma.
+Não invente percentual de confiança, eventos, comandos executados ou fontes.
+Regras determinísticas devem ser identificadas como regras, não como IA generativa.
+
+A integração deve ser opcional e privilegiar execução local. Inspecione o ambiente
+antes de escolher um provedor ou runtime. Separe o adaptador de inferência do
+scheduler e dos processos privilegiados do CUPS. Não exponha inferência ou
+credenciais diretamente no navegador. Defina limites de tempo, tamanho e acesso.
+
+Não habilite acesso externo automaticamente. Se houver provedor remoto, exija
+configuração explícita e mostre quais dados serão enviados. Nunca envie conteúdo
+dos documentos impressos, senhas ou tokens como contexto. Minimize e oculte dados
+pessoais e identificadores desnecessários nos logs usados para diagnóstico.
+
+Trate logs e respostas do modelo como dados não confiáveis: escape a saída,
+impeça HTML arbitrário e não interprete instruções contidas nos logs como comandos.
+A IA não deve executar shell nem alterar filas autonomamente. Eventuais ações
+integradas precisam de operações previamente definidas, validação no servidor,
+autorização existente, confirmação do usuário e registro do resultado real.
+
+Mostre estados explícitos: não configurada, consultando, resposta disponível,
+tempo excedido e falha de conexão. Sem integração real, mostre “IA não configurada”.
+Protótipos com respostas sintéticas devem estar marcados como demonstração.
+
+5. ACESSIBILIDADE E COMPORTAMENTO
+
+Garanta navegação por teclado, foco visível, nomes acessíveis, labels associados,
+hierarquia de títulos e cabeçalhos de tabela identificáveis.
+Verifique contraste de texto e controles; não use cinza muito escuro para conteúdo.
+Respeite prefers-reduced-motion. Transições discretas podem durar 120 a 180 ms.
+Evite atualizações que roubem foco ou interrompam leitura e formulários.
+
+Teste em larguras de 360, 768 e 1440 px e com zoom de 200%.
+Tabelas largas podem usar rolagem horizontal dentro de uma região identificada;
+o documento inteiro não deve transbordar horizontalmente.
+Garanta botões utilizáveis por toque e rodapé sem sobrepor informações.
+Preserve uma versão de impressão clara e econômica.
+
+6. IMPLEMENTAÇÃO E ENTREGA
+
+Execute em etapas:
+1. Registre o estado inicial, os fluxos existentes e alterações já feitas pelo usuário.
+2. Implemente tokens, estrutura responsiva e componentes sobre a interface atual.
+3. Atualize as páginas e traduções afetadas, preservando seus contratos funcionais.
+4. Implemente assistência contextual e integração opcional com limites explícitos.
+5. Valide instalação dos recursos, navegação, formulários e comportamento real.
+
+Não substitua todo o projeto por uma stack nova apenas para mudar o visual.
+Não adicione métricas, gráficos, menus ou botões que aparentem funcionar sem backend.
+Não altere configurações de rede, credenciais ou serviço como efeito colateral visual.
+
+Critérios de aceite:
+- A identidade remete a Unix com discrição e mantém a leitura confortável.
+- As telas principais apresentam uma linguagem visual consistente.
+- Dados, formulários, autenticação e ações existentes continuam funcionando.
+- A interface principal funciona sem serviços externos e sem IA.
+- Os recursos adicionados são incluídos na instalação e servidos corretamente.
+- O modo sem JavaScript preserva os fluxos essenciais que já eram disponíveis.
+- O diagnóstico diferencia evidência, hipótese e ação sugerida.
+- Estados vazios, erro, indisponibilidade e falta de permissão são compreensíveis.
+- Acessibilidade, responsividade e ausência de erros foram verificadas na medida
+ permitida pelo ambiente, com limitações descritas precisamente.
+
+Use verificações proporcionais às mudanças. Valide compilação e testes do CUPS
+quando houver alterações no código C; valide instalação e interface quando mudar
+recursos web. Faça smoke tests com filas e trabalhos isolados de demonstração,
+sem cancelar trabalhos reais nem enviar impressões físicas inesperadas.
+
+Entregue código, instruções para executar, capturas desktop/mobile e um resumo
+do que foi efetivamente testado. Separe funcionamento comprovado, demonstração
+e itens pendentes. Não anuncie IA integrada apenas porque a interface foi criada.
+
+RESULTADO ESPERADO
+
+Uma central de impressão com a precisão e a sobriedade de ferramentas Unix,
+acabamento visual contemporâneo e assistência de diagnóstico transparente.
+Pouco ruído. Informação confiável. Operação clara. Controle local.
diff --git a/doc/Makefile b/doc/Makefile
index e1eac766ed..477fc1f0c7 100644
--- a/doc/Makefile
+++ b/doc/Makefile
@@ -17,6 +17,7 @@ WEBPAGES = \
apple-touch-icon.png \
cups.css \
cups-printable.css \
+ cups.js \
robots.txt
WEBIMAGES = \
images/cups.png \
diff --git a/doc/cups-printable.css b/doc/cups-printable.css
index 0822138c80..da03435c14 100644
--- a/doc/cups-printable.css
+++ b/doc/cups-printable.css
@@ -1,436 +1,227 @@
-body {
- font-family: lucida grande, geneva, helvetica, arial, sans-serif;
-}
-
-h1, h2, h3, h4, h5, h6, p, td, th {
- font-family: lucida grande, geneva, helvetica, arial, sans-serif;
-}
-
-h1 { font-size: 2em; }
-h2 { font-size: 1.75em; }
-h3 { font-size: 1.5em; }
-h4 { font-size: 1.25em; }
-
-kbd {
- font-family: monaco, courier, monospace;
- font-weight: bold;
-}
-
-pre {
- font-family: monaco, courier, monospace;
-}
-
-blockquote {
- border-left: solid 2px #777;
- margin: 1em 0;
- padding: 10px;
-}
-
-blockquote ol li {
- margin-left: -1em;
-}
-
-pre.command, pre.example {
- padding: 10px 20px;
-}
-
-p.compact {
- margin: 0;
-}
-
-p.example {
- font-style: italic;
- margin-left: 36pt;
-}
-
-dl.man dd {
- margin-left: 5em;
-}
-
-dl.man dt {
- margin-left: 0;
-}
-
-pre.command em, pre.example em {
- font-family: lucida grande, geneva, helvetica, arial, sans-serif;
-}
-
-p.command {
- font-family: monaco, courier, monospace;
- margin-left: 36pt;
-}
-
-p.formula {
- font-style: italic;
- margin-left: 36pt;
-}
-
-a img {
- border: none;
-}
-
-a:link:hover img {
- background: #f0f0f0;
- border-radius: 10px;
- -moz-border-radius: 10px;
-}
-
-a:link, a:visited {
- font-weight: inherit;
- text-decoration: none;
-}
-
-a:link:hover, a:visited:hover, a:active {
- text-decoration: underline;
-}
-
-sub, sup {
- font-size: 50%;
-}
-
-tr.data, td.data, tr.data td {
- margin-top: 10pt;
- padding: 5pt;
- border-bottom: solid 1pt #999999;
-}
-
-tr.data th {
- border-bottom: solid 1pt #999999;
- padding-top: 10pt;
- padding-left: 5pt;
- text-align: left;
-}
-
-div.table table {
- border: solid thin #999999;
- border-collapse: collapse;
- border-spacing: 0;
- margin-left: auto;
- margin-right: auto;
-}
-
-div.table caption {
- caption-side: top;
- font-size: 120%;
- font-style: italic;
- font-weight: bold;
- margin-left: auto;
- margin-right: auto;
-}
-
-div.table table td {
- border: solid thin #cccccc;
- padding: 5pt 10pt 0;
-}
-
-div.table table th {
- background: #cccccc;
- border: none;
- border-bottom: solid thin #999999;
-}
-
-div.figure table {
- margin-left: auto;
- margin-right: auto;
-}
-
-div.figure caption {
- caption-side: bottom;
- font-size: 120%;
- font-style: italic;
- font-weight: bold;
- margin-left: auto;
- margin-right: auto;
-}
-
-th.label {
- text-align: right;
- vertical-align: top;
-}
-
-th.sublabel {
- text-align: right;
- font-weight: normal;
-}
-
-hr {
- border: solid thin;
-}
-
-span.info {
- background: black;
- border: thin solid black;
- color: white;
- font-size: 80%;
- font-style: italic;
- font-weight: bold;
- white-space: nowrap;
-}
-
-h2 span.info, h3 span.info, h4 span.info {
- float: right;
- font-size: 100%;
-}
+/*
+ * Modern Ink-Saving Printable Stylesheet for CUPS (OpenPrinting)
+ * Contemporary Unix Design System: Clean monochrome, zero decorative ink, crisp contrast.
+ */
-h1.title {
+@page {
+ margin: 1.5cm 1.5cm 1.5cm 1.5cm;
+ size: auto;
}
-h2.title, h3.title {
- border-bottom: solid 2pt #000000;
+:root {
+ --print-bg: #ffffff;
+ --print-text: #111111;
+ --print-muted: #444444;
+ --print-border: #888888;
+ --print-border-light: #cccccc;
}
-div.indent, table.indent {
- margin-top: 2em;
- margin-left: auto;
- margin-right: auto;
- width: 90%;
+* {
+ -webkit-print-color-adjust: exact !important;
+ print-color-adjust: exact !important;
+ box-shadow: none !important;
+ text-shadow: none !important;
}
-table.indent {
- border-collapse: collapse;
+body {
+ background: var(--print-bg) !important;
+ color: var(--print-text) !important;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif !important;
+ font-size: 11pt !important;
+ line-height: 1.4 !important;
+ margin: 0 !important;
+ padding: 0 !important;
+}
+
+/* Hide web-only interactive navigation, buttons, search forms, and shortcuts */
+.nav-container,
+.nav-subbar,
+.toolbar,
+form.search,
+.search-form,
+.cli-copy-btn,
+.diag-actions,
+.diag-llm-panel,
+.diag-llm-toggle,
+.button,
+button,
+input[type="submit"],
+input[type="button"],
+input[type="search"],
+.quick-action-bar,
+.unix-terminal-footer,
+.skip-link,
+#cups-shortcuts-modal {
+ display: none !important;
+}
+
+/* Preserve layout flow */
+.main-container {
+ max-width: 100% !important;
+ margin: 0 !important;
+ padding: 0 !important;
+}
+
+/* Titles and Headers */
+h1, h2, h3, h4, h5, h6 {
+ color: var(--print-text) !important;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif !important;
+ page-break-after: avoid;
+ margin-top: 12pt;
+ margin-bottom: 6pt;
}
-table.indent td, table.indent th {
- padding: 0;
+h1.title, h2.title {
+ font-size: 16pt !important;
+ border-bottom: 1.5pt solid var(--print-text) !important;
+ padding-bottom: 4pt !important;
}
-table.list {
- border-collapse: collapse;
- margin-left: auto;
- margin-right: auto;
- width: 90%;
+/* Unix Banners and Path Indicators */
+.unix-banner {
+ background: transparent !important;
+ border: 1pt solid var(--print-border) !important;
+ border-radius: 0 !important;
+ padding: 8pt 10pt !important;
+ margin-bottom: 12pt !important;
}
-table.list th {
- background: white;
- border-bottom: solid thin #cccccc;
- color: #444444;
- padding-top: 10pt;
- padding-left: 5pt;
- text-align: left;
- vertical-align: bottom;
- white-space: nowrap;
+.unix-banner-header {
+ font-size: 9pt !important;
+ color: var(--print-muted) !important;
+ margin-bottom: 4pt !important;
}
-table.list th a {
- color: #4444cc;
+.unix-path,
+.unix-prompt-mark {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
+ font-weight: bold !important;
+ color: var(--print-text) !important;
}
-table.list td {
- border-bottom: solid thin #eeeeee;
- padding-top: 5pt;
- padding-left: 5pt;
+/* Monospace technical labels and elements */
+code, kbd, pre, samp {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
+ font-size: 9.5pt !important;
}
-table.list tr:nth-child(even) {
- background: #f8f8f8;
+pre, pre.command, .cli-block {
+ background: #f8f8f8 !important;
+ border: 1pt solid var(--print-border-light) !important;
+ border-radius: 2pt !important;
+ color: var(--print-text) !important;
+ padding: 6pt 8pt !important;
+ white-space: pre-wrap !important;
+ word-break: break-all !important;
+ page-break-inside: avoid;
}
-table.list tr:nth-child(odd) {
- background: #f4f4f4;
+/* Tables */
+table, table.list, table.dashboard-table {
+ width: 100% !important;
+ border-collapse: collapse !important;
+ margin-top: 8pt !important;
+ margin-bottom: 14pt !important;
+ page-break-inside: auto;
}
-dt {
- margin-left: 36pt;
- margin-top: 12pt;
+tr {
+ page-break-inside: avoid;
+ page-break-after: auto;
+}
+
+thead th, th {
+ background: #f0f0f0 !important;
+ color: var(--print-text) !important;
+ border-top: 1.5pt solid var(--print-text) !important;
+ border-bottom: 1pt solid var(--print-text) !important;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
+ font-size: 9pt !important;
+ font-weight: bold !important;
+ text-transform: uppercase !important;
+ padding: 5pt 6pt !important;
+ text-align: left !important;
+}
+
+td {
+ border-bottom: 1pt solid var(--print-border-light) !important;
+ padding: 5pt 6pt !important;
+ font-size: 9.5pt !important;
+}
+
+/* Status Pills for Ink-Saving Print */
+.status-pill {
+ display: inline-block !important;
+ border: 1pt solid var(--print-border) !important;
+ background: transparent !important;
+ color: var(--print-text) !important;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
+ font-size: 8.5pt !important;
+ font-weight: bold !important;
+ padding: 1pt 4pt !important;
+ border-radius: 2pt !important;
+ text-transform: uppercase !important;
+}
+
+/* Diagnostic Panels */
+.diag-panel {
+ background: transparent !important;
+ border: 1pt solid var(--print-border) !important;
+ border-radius: 0 !important;
+ padding: 8pt 10pt !important;
+ margin-top: 12pt !important;
+ page-break-inside: avoid;
}
-dd {
- margin-left: 54pt;
+.diag-header {
+ border-bottom: 1pt solid var(--print-border-light) !important;
+ padding-bottom: 4pt !important;
+ margin-bottom: 8pt !important;
}
-dl.category dt {
- font-weight: bold;
+.diag-grid {
+ display: block !important;
}
-p.summary {
- margin-left: 36pt;
- font-family: monaco, courier, monospace;
+.diag-card {
+ background: transparent !important;
+ border: 1pt solid var(--print-border-light) !important;
+ border-left: 3pt solid var(--print-text) !important;
+ border-radius: 0 !important;
+ padding: 6pt 8pt !important;
+ margin-bottom: 6pt !important;
+ page-break-inside: avoid;
}
-div.summary table {
- border: solid thin #999999;
- border-collapse: collapse;
- border-spacing: 0;
- margin: 10px;
+.diag-card-label {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important;
+ font-size: 8.5pt !important;
+ font-weight: bold !important;
+ color: var(--print-muted) !important;
+ margin-bottom: 2pt !important;
}
-div.summary table td, div.summary table th {
- border: solid thin #999999;
- padding: 5px;
- text-align: left;
- vertical-align: top;
+.diag-card-content {
+ font-size: 9.5pt !important;
}
-div.summary table thead th {
- background: #eeeeee;
+/* Links */
+a {
+ color: var(--print-text) !important;
+ text-decoration: underline !important;
}
-/* API documentation styles... */
-div.body h1 {
- font-size: 250%;
- font-weight: bold;
- margin: 0;
-}
-div.body h2 {
- font-size: 250%;
- margin-top: 1.5em;
-}
-div.body h3 {
- font-size: 150%;
- margin-bottom: 0.5em;
- margin-top: 1.5em;
-}
-div.body h4 {
- font-size: 110%;
- margin-bottom: 0.5em;
- margin-top: 1.5em;
-}
-div.body h5 {
- font-size: 100%;
- margin-bottom: 0.5em;
- margin-top: 1.5em;
-}
-/*div.contents {
-}*/
-div.contents h1 {
- font-size: 110%;
-}
-div.contents h2 {
- font-size: 100%;
-}
-div.contents ul.contents {
- font-size: 80%;
-}
-.class {
- border-bottom: solid 2px gray;
-}
-.constants {
-}
-.description {
- margin-top: 0.5em;
-}
-.discussion {
-}
-.enumeration {
- border-bottom: solid 2px gray;
-}
-.function {
- border-bottom: solid 2px gray;
- margin-bottom: 0;
-}
-.members {
-}
-.method {
-}
-.parameters {
-}
-.returnvalue {
-}
-.struct {
- border-bottom: solid 2px gray;
-}
-.typedef {
- border-bottom: solid 2px gray;
-}
-.union {
- border-bottom: solid 2px gray;
-}
-.variable {
-}
-h1, h2, h3, h4, h5, h6 {
- page-break-inside: avoid;
-}
-blockquote {
- background: rgba(191,191,191,0.1);
- border-left: solid 2px #777;
- margin: 1em 0;
- padding: 10px;
+/* Alert boxes */
+.alert-box {
+ background: #fbfbfb !important;
+ border: 1pt solid var(--print-border) !important;
+ color: var(--print-text) !important;
+ padding: 6pt 8pt !important;
+ margin-bottom: 10pt !important;
page-break-inside: avoid;
}
-p.code, pre, ul.code li {
- border-left: thin dotted gray;
- background: rgba(191,191,191,0.1);
-}
-p code, li code {
- background: rgba(191,191,191,0.3);
-}
-p code, li code, p.code, pre, ul.code li {
- font-family: monospace;
- hyphens: manual;
- margin: 18pt 0;
- -webkit-hyphens: manual;
- page-break-inside: avoid;
-}
-p.code, pre, ul.code li {
- padding: 10px;
-}
-p code, li code {
- padding: 2px 5px;
-}
-a:link, a:visited {
- text-decoration: none;
-}
-span.info {
- background: black;
- border: solid thin black;
- color: white;
- font-size: 80%;
- font-style: italic;
- font-weight: bold;
- white-space: nowrap;
-}
-h2 span.info, h3 span.info, h4 span.info {
- border-radius: 10px;
- float: right;
- font-size: 80%;
- padding: 3px 6px;
-}
-h2.title span.info, h3.title span.info, h4.title span.info {
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
-}
-h2.title span.info {
- padding: 4px 6px;
-}
-ul.code, ul.contents, ul.subcontents {
- list-style-type: none;
- margin: 0;
- padding-left: 0;
-}
-ul.code li {
- margin: 0;
-}
-ul.contents > li {
- margin-top: 1em;
-}
-ul.contents li ul.code, ul.contents li ul.subcontents {
- padding-left: 2em;
-}
-table.list {
- border-collapse: collapse;
- width: 100%;
-}
-table.list tr:nth-child(even) {
- background: rgba(127,127,127,0.1);
-}
-table.list th {
- border-right: 2px solid gray;
- font-family: monospace;
- padding: 5px 10px 5px 2px;
- text-align: right;
- vertical-align: top;
-}
-table.list td {
- padding: 5px 2px 5px 10px;
- text-align: left;
- vertical-align: top;
-}
-h1.title {
-}
-h2.title {
- border-bottom: solid 2px black;
-}
-h3.title {
- border-bottom: solid 2px black;
+
+.alert-title {
+ font-weight: bold !important;
+ margin-bottom: 2pt !important;
}
diff --git a/doc/cups.css b/doc/cups.css
index a5cfbbff4c..5eed4e6a7a 100644
--- a/doc/cups.css
+++ b/doc/cups.css
@@ -1,797 +1,2009 @@
-/* Base styles */
-@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,400;0,700;1,400;1,700&family=Source+Code+Pro&display=swap');
+/*
+ * OpenPrinting CUPS - Root Intelligence Web Interface Styles
+ * Direção Visual: UNIX Contemporâneo
+ * Autonomia, transparência, precisão de terminal, controle local.
+ *
+ * Copyright © 2020-2026 by OpenPrinting.
+ * Licensed under Apache License v2.0.
+ */
+
+/* ==========================================================================
+ 1. DESIGN TOKENS (CSS Custom Properties)
+ ========================================================================== */
+:root {
+ /* Paleta Unix Contemporâneo - Fundo Grafite Quase Preto */
+ --cups-bg-main: #101211; /* Fundo principal da página */
+ --cups-bg-surface: #181C19; /* Superfície padrão (header, cards, tabelas) */
+ --cups-bg-elevated: #202622; /* Superfície elevada (cabeçalhos, modais, painéis) */
+ --cups-bg-input: #131714; /* Campos de entrada e texto */
+ --cups-bg-zebra: #141715; /* Linhas alternadas de tabelas */
+ --cups-bg-hover: #262E29; /* Estado hover em superfícies */
+
+ /* Divisores e Bordas Finais */
+ --cups-border-subtle: #242B26; /* Divisores internos discretos */
+ --cups-border: #343D36; /* Borda padrão de 1px */
+ --cups-border-strong: #455248; /* Borda destacada */
+ --cups-border-focus: #A8C686; /* Borda de foco / seleção ativa */
+
+ /* Tipografia e Contraste Estrito (Verificado WCAG AAA) */
+ --cups-text-primary: #EDF2EC; /* Texto principal (Contraste 14.9:1) */
+ --cups-text-secondary: #B0BCAF; /* Texto secundário e rótulos (Contraste 8.7:1) */
+ --cups-text-muted: #7E8C7E; /* Metadados e comentários (Contraste 4.8:1) */
+ --cups-text-code: #C8D8C5; /* Código e identificadores técnicos */
+
+ /* Acentos Funcionais Pontuais */
+ --cups-accent-green: #A8C686; /* Destaque verde sálvia: seleção, foco, ação principal */
+ --cups-accent-green-bg: rgba(168, 198, 134, 0.12);
+ --cups-accent-green-hover: #BCD89A;
+
+ --cups-state-amber: #E4BC75; /* Atenção âmbar: pausada, retida, alerta */
+ --cups-state-amber-bg: rgba(228, 188, 117, 0.12);
+
+ --cups-state-coral: #EF9990; /* Erro coral: parado, abortado, cancelamento */
+ --cups-state-coral-bg: rgba(239, 153, 144, 0.12);
+
+ --cups-state-blue: #9FBFCB; /* Informação azul: processando, conectividade */
+ --cups-state-blue-bg: rgba(159, 191, 203, 0.12);
+
+ /* Escala de Espaçamento Estrita (4, 8, 12, 16, 24, 32, 48 px) */
+ --space-1: 4px;
+ --space-2: 8px;
+ --space-3: 12px;
+ --space-4: 16px;
+ --space-6: 24px;
+ --space-8: 32px;
+ --space-12: 48px;
+
+ /* Raios de Borda Contidos (4px a 8px) */
+ --radius-sm: 4px;
+ --radius-md: 6px;
+ --radius-lg: 8px;
+
+ /* Fontes de Sistema Nativas (Zero Dependência de CDN Remota / 100% Offline) */
+ --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+ --font-mono: ui-monospace, "SF Mono", "Cascadia Code", "Cascadia Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace;
+
+ --font-size-xs: 11px;
+ --font-size-sm: 13px;
+ --font-size-base: 15px;
+ --font-size-md: 17px;
+ --font-size-lg: 20px;
+ --font-size-xl: 24px;
+ --line-height-base: 1.5;
+
+ --shadow-subtle: 0 1px 3px rgba(0, 0, 0, 0.4);
+ --shadow-card: 0 4px 12px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(255, 255, 255, 0.04);
+ --shadow-elevated: 0 8px 24px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(168, 198, 134, 0.12);
+ --shadow-glow-green: 0 0 12px rgba(168, 198, 134, 0.25);
+
+ --transition-fast: 120ms cubic-bezier(0.4, 0, 0.2, 1);
+ --transition-normal: 180ms cubic-bezier(0.4, 0, 0.2, 1);
+ --transition-spring: 260ms cubic-bezier(0.16, 1, 0.3, 1);
+}
+
+/* ==========================================================================
+ 2. BASE & RESET
+ ========================================================================== */
+*, *::before, *::after {
+ box-sizing: border-box;
+}
+
+html {
+ background-color: var(--cups-bg-main);
+ color: var(--cups-text-primary);
+ font-family: var(--font-sans);
+ font-size: var(--font-size-base);
+ line-height: var(--line-height-base);
+ -webkit-text-size-adjust: 100%;
+ text-rendering: optimizeLegibility;
+}
-/* Layout CSS */
-.cups-header {
- background: rgba(46,46,46,.9);
- box-shadow: 0 2px 5px rgba(0,0,0,0.25);
- color: white;
- left: 0;
- margin-bottom: 20px;
+body {
+ background-color: var(--cups-bg-main);
+ color: var(--cups-text-primary);
+ margin: 0;
padding: 0;
- position: fixed;
- right: 0;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+/* ==========================================================================
+ 3. TIPOGRAFIA & ELEMENTOS BÁSICOS
+ ========================================================================== */
+h1, h2, h3, h4, h5, h6 {
+ color: var(--cups-text-primary);
+ font-family: var(--font-sans);
+ font-weight: 600;
+ line-height: 1.25;
+ margin-top: var(--space-6);
+ margin-bottom: var(--space-3);
+ letter-spacing: -0.01em;
+}
+
+h1 { font-size: var(--font-size-xl); }
+h2 { font-size: var(--font-size-lg); }
+h3 { font-size: var(--font-size-md); }
+h4 { font-size: var(--font-size-base); }
+
+h1.title, h2.title, h3.title {
+ border-bottom: 1px solid var(--cups-border);
+ padding-bottom: var(--space-2);
+ margin-top: var(--space-4);
+ margin-bottom: var(--space-4);
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+}
+
+p, ul, ol, dl {
+ margin-top: 0;
+ margin-bottom: var(--space-4);
+ color: var(--cups-text-secondary);
+}
+
+p:last-child {
+ margin-bottom: 0;
+}
+
+a {
+ color: var(--cups-accent-green);
+ text-decoration: none;
+ transition: color 140ms ease;
+}
+
+a:hover, a:focus-visible {
+ color: var(--cups-accent-green-hover);
+ text-decoration: underline;
+ text-underline-offset: 3px;
+}
+
+/* Identificadores técnicos, comandos, números tabulares */
+code, kbd, pre, samp, tt,
+input[type="text"], input[type="search"], textarea, select,
+.mono-val, .job-id, .queue-name, .timestamp {
+ font-family: var(--font-mono);
+ font-variant-numeric: tabular-nums;
+}
+
+code {
+ background-color: var(--cups-bg-elevated);
+ border: 1px solid var(--cups-border-subtle);
+ border-radius: var(--radius-sm);
+ color: var(--cups-text-code);
+ font-size: 0.9em;
+ padding: 2px 6px;
+}
+
+kbd {
+ background-color: var(--cups-bg-elevated);
+ border: 1px solid var(--cups-border);
+ border-bottom-width: 2px;
+ border-radius: var(--radius-sm);
+ color: var(--cups-accent-green);
+ font-size: 0.85em;
+ font-weight: 600;
+ padding: 2px 5px;
+}
+
+pre, pre.command, pre.example {
+ background-color: var(--cups-bg-surface);
+ border: 1px solid var(--cups-border);
+ border-left: 3px solid var(--cups-accent-green);
+ border-radius: var(--radius-md);
+ color: var(--cups-text-primary);
+ font-size: var(--font-size-sm);
+ line-height: 1.45;
+ margin: var(--space-4) 0;
+ overflow-x: auto;
+ padding: var(--space-3) var(--space-4);
+ position: relative;
+ white-space: pre-wrap;
+}
+
+blockquote {
+ background-color: var(--cups-bg-surface);
+ border-left: 3px solid var(--cups-border-strong);
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
+ color: var(--cups-text-secondary);
+ margin: var(--space-4) 0;
+ padding: var(--space-3) var(--space-4);
+}
+
+hr {
+ border: 0;
+ border-top: 1px solid var(--cups-border);
+ margin: var(--space-6) 0;
+}
+
+/* ==========================================================================
+ 4. LAYOUT PRINCIPAL (Barra Superior, Corpo, Rodapé)
+ ========================================================================== */
+
+/* Cabeçalho Fixo / Barra Superior Compacta com Glassmorphism */
+.cups-header {
+ background: rgba(24, 28, 25, 0.88);
+ backdrop-filter: blur(14px);
+ -webkit-backdrop-filter: blur(14px);
+ border-bottom: 1px solid var(--cups-border);
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.45);
+ position: sticky;
top: 0;
width: 100%;
+ z-index: 1000;
}
+
.cups-header ul {
- font-size: 20px;
+ align-items: center;
+ display: flex;
+ flex-wrap: wrap;
list-style: none;
margin: 0;
- -webkit-margin-before: 0;
- -webkit-margin-after: 0;
- -webkit-margin-start: 0;
- -webkit-margin-end: 5px;
- -webkit-padding-start: 0;
+ max-width: 1440px;
+ min-height: 52px;
+ padding: 0 var(--space-4);
}
+
.cups-header ul li {
- float: left;
+ display: flex;
+ align-items: center;
}
+
.cups-header ul li.right {
- float: right;
+ margin-left: auto;
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
}
-.cups-header a {
+
+.cups-header a.img {
+ display: flex;
+ align-items: center;
+ padding: var(--space-2) var(--space-3) var(--space-2) 0 !important;
+ gap: var(--space-2);
+}
+
+.cups-header a.img img {
+ border-radius: var(--radius-sm);
display: block;
- padding: 18px 30px !important;
}
-.cups-header a.img {
- padding: 16px 24px 8px !important;
+
+/* Marcador Unix '>_' junto à identidade do CUPS */
+.prompt-marker, .prompt-glyph {
+ color: var(--cups-accent-green);
+ font-family: var(--font-mono);
+ font-size: var(--font-size-md);
+ font-weight: 700;
+ user-select: none;
}
-.cups-header a:link, .cups-header a:visited {
- color: white !important;
+
+.cups-header a {
+ border-bottom: 2px solid transparent;
+ color: var(--cups-text-secondary) !important;
+ display: inline-flex;
+ align-items: center;
+ font-size: var(--font-size-sm);
+ font-weight: 500;
+ padding: 16px var(--space-3) !important;
text-decoration: none !important;
+ transition: all 140ms ease;
}
+
.cups-header a:hover {
- background: #cccccc !important;
- color: #333333 !important;
- text-decoration: none !important;
+ background-color: var(--cups-bg-elevated) !important;
+ color: var(--cups-text-primary) !important;
}
+
.cups-header a.active {
- background: white !important;
- box-shadow: rgba(0,0,0,0.1) 0 0 10px 0 inset;
- color: black !important;
- text-decoration: none !important;
+ background-color: transparent !important;
+ border-bottom-color: var(--cups-accent-green) !important;
+ box-shadow: none !important;
+ color: var(--cups-accent-green) !important;
+ font-weight: 600;
}
+
+/* Formulário de busca no cabeçalho */
.cups-header form {
- display: block;
- padding: 15px 15px !important;
+ align-items: center;
+ display: flex;
+ margin: 0;
+ padding: 0 !important;
+ gap: var(--space-1);
+}
+
+.cups-header input[type="search"] {
+ background-color: var(--cups-bg-input);
+ border: 1px solid var(--cups-border);
+ border-radius: var(--radius-sm);
+ color: var(--cups-text-primary);
+ font-size: var(--font-size-sm);
+ padding: 6px 10px;
+ width: 140px;
+ transition: all 140ms ease;
}
+
+.cups-header input[type="search"]:focus {
+ border-color: var(--cups-accent-green);
+ outline: 2px solid var(--cups-accent-green-bg);
+ width: 180px;
+}
+
.cups-header span.label {
- color: #ccc;
- display: block;
- font-style: italic;
- padding: 15px 0 !important;
+ color: var(--cups-text-muted);
+ font-family: var(--font-mono);
+ font-size: var(--font-size-xs);
+ padding: 0 !important;
+}
+
+/* Botão de Acesso Rápido à Paleta de Comandos (Ctrl+K) */
+.btn-cmd-palette {
+ align-items: center;
+ background: var(--cups-bg-input);
+ border: 1px solid var(--cups-border);
+ border-radius: var(--radius-sm);
+ color: var(--cups-text-secondary);
+ cursor: pointer;
+ display: inline-flex;
+ font-family: var(--font-sans);
+ font-size: var(--font-size-xs);
+ gap: 6px;
+ margin: 0 var(--space-2);
+ padding: 5px 10px;
+ transition: all var(--transition-normal);
}
-.cups-body {
- margin-bottom: 20px;
- margin-top: 65px;
- padding: 20px;
+.btn-cmd-palette:hover {
+ background: var(--cups-bg-elevated);
+ border-color: var(--cups-accent-green);
+ color: var(--cups-text-primary);
+ box-shadow: 0 0 10px rgba(168, 198, 134, 0.2);
}
-.cups-body .row {
- padding: 0;
+
+.btn-cmd-palette:active {
+ transform: scale(0.96);
}
-.cups-footer {
- background: rgba(46,46,46,.9);
- bottom: 0;
- box-shadow: 0 -2px 5px rgba(0,0,0,0.25);
- color: #cccccc;
+.btn-cmd-palette .cmd-icon {
+ color: var(--cups-accent-green);
+ font-family: var(--font-mono);
+ font-weight: 700;
+}
+
+.btn-cmd-palette .kbd-badge {
+ background: rgba(255, 255, 255, 0.08);
+ border: 1px solid var(--cups-border);
+ border-radius: 3px;
+ color: var(--cups-text-muted);
+ font-family: var(--font-mono);
font-size: 10px;
- height: 20px;
- left: 0;
- padding: 10px 10px 3px;
- position: fixed;
- width: 100%;
+ padding: 1px 5px;
}
-.cups-footer a:link, footer a:hover, .cups-footer a:visited {
- color: white !important;
- text-decoration: none !important;
+
+/* Chave Seletora de Modo (Iniciante / Avançado) */
+.mode-toggle-btn {
+ align-items: center;
+ background: rgba(24, 28, 25, 0.9);
+ border: 1px solid var(--cups-border);
+ border-radius: 20px;
+ color: var(--cups-text-primary);
+ cursor: pointer;
+ display: inline-flex;
+ font-family: var(--font-sans);
+ font-size: var(--font-size-xs);
+ font-weight: 500;
+ gap: 8px;
+ margin: 0 var(--space-2);
+ padding: 5px 12px;
+ transition: all var(--transition-normal);
+}
+
+.mode-toggle-btn:hover {
+ border-color: var(--cups-accent-green);
+ box-shadow: 0 0 12px rgba(168, 198, 134, 0.25);
+ transform: translateY(-1px);
+}
+
+.mode-toggle-btn:active {
+ transform: scale(0.96);
+}
+
+.mode-indicator-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background-color: var(--cups-accent-green);
+ box-shadow: 0 0 8px var(--cups-accent-green);
+ display: inline-block;
+ transition: all var(--transition-normal);
+}
+
+/* Modo Iniciante Ativo */
+html.mode-beginner .mode-indicator-dot {
+ background-color: var(--cups-accent-green);
+ box-shadow: 0 0 8px var(--cups-accent-green);
+}
+html.mode-beginner .mode-toggle-btn {
+ border-color: rgba(168, 198, 134, 0.4);
+}
+
+/* Modo Avançado Ativo */
+html.mode-advanced .mode-indicator-dot {
+ background-color: var(--cups-state-amber);
+ box-shadow: 0 0 8px var(--cups-state-amber);
+}
+html.mode-advanced .mode-toggle-btn {
+ border-color: rgba(228, 188, 117, 0.4);
+}
+
+/* Corpo Principal */
+.cups-body {
+ flex: 1 0 auto;
+ margin-bottom: var(--space-12);
+ margin-top: 0;
+ padding: var(--space-6) var(--space-4);
+ max-width: 1440px;
+ width: 100%;
+ margin-left: auto;
+ margin-right: auto;
}
-.row {
+.cups-body .row {
+ margin-bottom: var(--space-6);
+ padding: 0;
width: 100%;
- *zoom: 1;
}
-.row:after {
+
+.row::after {
+ content: "";
+ display: table;
clear: both;
}
-.row .thirds {
+/* Painéis Tmux-like e Divisões */
+.row .halves {
float: left;
- margin-left: 0.5%;
- margin-right: 0;
- padding-bottom: 40px;
- width: 33%;
+ margin-left: 1.5%;
+ width: 49.25%;
}
-.row .thirds:first-child {
+
+.row .halves:first-child {
margin-left: 0;
}
-.row .halves {
+.row .thirds {
float: left;
- margin-left: 0.5%;
- margin-right: 0;
- padding-bottom: 40px;
- width: 49.75%;
+ margin-left: 1.5%;
+ width: 32.33%;
}
-.row .halves:first-child {
+
+.row .thirds:first-child {
margin-left: 0;
}
-.row .thirds h1, .row .halves h1, .row h1 {
- margin-top: 0;
+/* Rodapé Contemporâneo */
+.cups-footer {
+ background-color: var(--cups-bg-surface);
+ border-top: 1px solid var(--cups-border);
+ color: var(--cups-text-muted);
+ font-family: var(--font-mono);
+ font-size: var(--font-size-xs);
+ min-height: 38px;
+ padding: var(--space-2) var(--space-4);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: var(--space-3);
+ width: 100%;
+ z-index: 100;
}
-.mobile {
- display: none;
+.cups-footer a {
+ color: var(--cups-text-secondary) !important;
}
-.no-mobile {
- display: inherit;
+
+.cups-footer a:hover {
+ color: var(--cups-accent-green) !important;
}
-/* Appearance CSS */
-body {
- background: white;
- color: black;
- font-family: 'Open Sans', sans-serif;
- font-size: 16px;
- margin: 0;
+/* ==========================================================================
+ 5. CONTROLES DE FORMULÁRIO & BOTÕES
+ ========================================================================== */
+button, input[type="button"], input[type="submit"], input[type="reset"], a.button {
+ align-items: center;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.04) 0%, rgba(0, 0, 0, 0.12) 100%), var(--cups-bg-elevated);
+ border: 1px solid var(--cups-border);
+ border-radius: var(--radius-sm);
+ color: var(--cups-text-primary);
+ cursor: pointer;
+ display: inline-flex;
+ font-family: var(--font-sans);
+ font-size: var(--font-size-sm);
+ font-weight: 500;
+ gap: var(--space-2);
+ justify-content: center;
+ line-height: 1.4;
+ padding: 6px 14px;
+ text-decoration: none;
+ transition: transform var(--transition-fast), background-color var(--transition-fast), border-color var(--transition-fast), box-shadow var(--transition-fast), color var(--transition-fast);
+ user-select: none;
+ vertical-align: middle;
+ white-space: nowrap;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.05);
+ position: relative;
+}
+
+button:hover, input[type="button"]:hover, input[type="submit"]:hover, a.button:hover {
+ background-color: var(--cups-bg-hover);
+ border-color: var(--cups-border-strong);
+ color: var(--cups-text-primary);
+ transform: translateY(-1px);
+ box-shadow: 0 3px 8px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.08);
+}
+
+button:active, input[type="button"]:active, input[type="submit"]:active, a.button:active {
+ background-color: var(--cups-bg-surface);
+ transform: translateY(1px);
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.6);
+}
+
+button:focus-visible, input[type="submit"]:focus-visible, input[type="button"]:focus-visible, a.button:focus-visible {
+ outline: 2px solid var(--cups-accent-green);
+ outline-offset: 2px;
+}
+
+/* Botões de Ação Primária */
+.btn-primary,
+form[action="/admin/"] input[type="submit"][value*="Adicionar"],
+form[action="/admin/"] input[type="submit"][value*="Add"],
+input[type="submit"][name="SEARCH"],
+input[type="submit"][value="Pesquisar"],
+input[type="submit"][value="Search"],
+input[type="submit"][value="Continue"],
+input[type="submit"][value="Alterar configurações"],
+input[type="submit"][value="Change Settings"] {
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0.08) 100%), var(--cups-accent-green);
+ border-color: var(--cups-accent-green);
+ color: #0c0f0d !important;
+ font-weight: 600;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4), 0 0 10px rgba(168, 198, 134, 0.22);
+}
+
+.btn-primary:hover,
+form[action="/admin/"] input[type="submit"][value*="Adicionar"]:hover,
+form[action="/admin/"] input[type="submit"][value*="Add"]:hover,
+input[type="submit"][name="SEARCH"]:hover,
+input[type="submit"][value="Pesquisar"]:hover,
+input[type="submit"][value="Search"]:hover,
+input[type="submit"][value="Continue"]:hover,
+input[type="submit"][value="Alterar configurações"]:hover,
+input[type="submit"][value="Change Settings"]:hover {
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.25) 0%, rgba(0, 0, 0, 0.04) 100%), var(--cups-accent-green-hover);
+ border-color: var(--cups-accent-green-hover);
+ color: #080a09 !important;
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5), 0 0 14px rgba(168, 198, 134, 0.38);
+}
+
+/* Botões Destrutivos (Cancelar, Excluir) */
+input[type="submit"][value*="Cancel"],
+input[type="submit"][value*="Cancelar"],
+input[type="submit"][value*="Delete"],
+input[type="submit"][value*="Excluir"] {
+ border-color: rgba(239, 153, 144, 0.45);
+ color: var(--cups-state-coral);
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
+}
+
+input[type="submit"][value*="Cancel"]:hover,
+input[type="submit"][value*="Cancelar"]:hover,
+input[type="submit"][value*="Delete"]:hover,
+input[type="submit"][value*="Excluir"]:hover {
+ background: linear-gradient(180deg, rgba(239, 153, 144, 0.18) 0%, rgba(0, 0, 0, 0.2) 100%), var(--cups-state-coral-bg);
+ border-color: var(--cups-state-coral);
+ color: #fff;
+ transform: translateY(-1px);
+ box-shadow: 0 3px 10px rgba(239, 153, 144, 0.28);
+}
+
+/* Entradas de Texto */
+input[type="text"], input[type="search"], input[type="password"], textarea {
+ background-color: var(--cups-bg-input);
+ border: 1px solid var(--cups-border);
+ border-radius: var(--radius-sm);
+ color: var(--cups-text-primary);
+ font-family: var(--font-sans);
+ font-size: var(--font-size-sm);
+ line-height: 1.4;
+ padding: 6px 10px;
+ transition: all 140ms ease;
+}
+
+input[type="text"]:focus, input[type="search"]:focus, textarea:focus {
+ background-color: var(--cups-bg-surface);
+ border-color: var(--cups-accent-green);
+ outline: 2px solid var(--cups-accent-green-bg);
+ outline-offset: 1px;
+}
+
+select {
+ background-color: var(--cups-bg-elevated);
+ border: 1px solid var(--cups-border);
+ border-radius: var(--radius-sm);
+ color: var(--cups-text-primary);
+ font-family: var(--font-sans);
+ font-size: var(--font-size-sm);
+ padding: 6px 30px 6px 10px;
+ cursor: pointer;
+ background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3e%3cpath fill='%23B0BCAF' d='M0 0l5 6 5-6z'/%3e%3c/svg%3e");
+ background-repeat: no-repeat;
+ background-position: right 10px center;
+ background-size: 8px 5px;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
}
-h1, h2, h3, h4, h5, h6, p, td, th {
- font-family: 'Open Sans', sans-serif;
+select:hover {
+ border-color: var(--cups-border-strong);
}
-h1 { font-size: 2em; }
-h2 { font-size: 1.75em; }
-h3 { font-size: 1.5em; }
-h4 { font-size: 1.25em; }
+select:focus {
+ border-color: var(--cups-accent-green);
+ outline: 2px solid var(--cups-accent-green-bg);
+}
-code, kbd, pre, tt, input[type=text], p.command, p.summary, p code, li code, p.code, ul.code li, textarea {
- font-family: 'Source Code Pro', monospace;
+/* Checkbox e Radio Acessíveis */
+input[type="checkbox"], input[type="radio"] {
+ accent-color: var(--cups-accent-green);
+ cursor: pointer;
+ margin-right: var(--space-2);
+ vertical-align: middle;
}
-kbd {
- color: #006600;
- font-weight: bold;
+label {
+ color: var(--cups-text-primary);
+ cursor: pointer;
}
-pre {
- white-space: pre-wrap;
+/* ==========================================================================
+ 6. TABELAS (Estilo Terminal / Man Page / htop)
+ ========================================================================== */
+table {
+ border-collapse: collapse;
+ margin-bottom: var(--space-6);
+ width: 100%;
}
-blockquote {
- background: rgba(191,191,191,0.1);
- border-left: solid 2px #777;
- margin: 1em 0;
- padding: 10px;
+table.list {
+ background-color: var(--cups-bg-surface);
+ border: 1px solid var(--cups-border);
+ border-radius: var(--radius-md);
+ margin-top: var(--space-4);
+ overflow: hidden;
+ width: 100%;
}
-blockquote ol li {
- margin-left: -1em;
+table.list thead th,
+table.list th {
+ background-color: var(--cups-bg-elevated);
+ border: none;
+ border-bottom: 1px solid var(--cups-border);
+ color: var(--cups-text-secondary);
+ font-family: var(--font-mono);
+ font-size: var(--font-size-xs);
+ font-weight: 600;
+ letter-spacing: 0.05em;
+ padding: 10px 14px;
+ text-align: left;
+ text-transform: uppercase;
+ white-space: nowrap;
}
-pre.command, pre.example {
- padding: 10px 20px;
+table.list td {
+ border: none;
+ border-bottom: 1px solid var(--cups-border-subtle);
+ color: var(--cups-text-primary);
+ font-size: var(--font-size-sm);
+ padding: 10px 14px;
+ vertical-align: middle;
}
-p.example {
- font-style: italic;
- margin-left: 36pt;
+table.list tbody tr {
+ transition: background-color var(--transition-fast), box-shadow var(--transition-fast);
}
-dl.man dd {
- margin-left: 5em;
+table.list tbody tr:nth-child(even) {
+ background-color: var(--cups-bg-zebra);
}
-DL.man DT {
- margin-left: 0;
+table.list tbody tr:nth-child(odd) {
+ background-color: var(--cups-bg-surface);
}
-pre.man {
- margin: 0;
+table.list tbody tr:hover {
+ background-color: var(--cups-bg-hover);
+ box-shadow: inset 3px 0 0 var(--cups-accent-green);
}
-pre.command EM, pre.example EM {
- color: #3f0000;
+table.list tr:last-child td {
+ border-bottom: none;
}
-p.command {
- color: #7f0000;
- margin-left: 36pt;
+/* Tabela de atributos / detalhes (ex: printer.tmpl) */
+table.indent, table.summary, table.inset {
+ background-color: var(--cups-bg-surface);
+ border: 1px solid var(--cups-border);
+ border-radius: var(--radius-md);
+ margin-top: var(--space-4);
+ padding: var(--space-3);
}
-p.formula {
- font-style: italic;
- margin-left: 36pt;
+table.indent th, table.summary th, th.label {
+ color: var(--cups-text-secondary);
+ font-family: var(--font-mono);
+ font-size: var(--font-size-sm);
+ font-weight: 500;
+ padding: 8px 12px;
+ text-align: right;
+ vertical-align: top;
+ width: 25%;
}
-a img {
- border: none;
+table.indent td, table.summary td {
+ color: var(--cups-text-primary);
+ font-size: var(--font-size-sm);
+ padding: 8px 12px;
+ vertical-align: top;
}
-a:link:hover img {
- background: #f0f0f0;
- border-radius: 10px;
- -moz-border-radius: 10px;
+/* ==========================================================================
+ 7. BADGES DE STATUS & SINAIS OPERACIONAIS
+ ========================================================================== */
+.status-pill {
+ align-items: center;
+ border-radius: var(--radius-sm);
+ display: inline-flex;
+ font-family: var(--font-mono);
+ font-size: var(--font-size-xs);
+ font-weight: 600;
+ gap: 6px;
+ padding: 3px 8px;
+ white-space: nowrap;
+ transition: all var(--transition-fast);
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
}
-a:link, a:visited {
- font-weight: inherit;
- text-decoration: none;
- color: #317BFF;
+.status-pill:hover {
+ transform: translateY(-1px);
}
-a:link:hover, a:visited:hover, a:active {
- text-decoration: underline;
- color: #4C80BF;
+.status-pill.idle {
+ background-color: var(--cups-accent-green-bg);
+ border: 1px solid var(--cups-accent-green);
+ color: var(--cups-accent-green);
}
-table.page {
- border: none;
- border-collapse: collapse;
- height: 100%;
- margin: 0;
- padding: 0;
- width: 100%;
+.status-pill.processing {
+ background-color: var(--cups-state-blue-bg);
+ border: 1px solid var(--cups-state-blue);
+ color: var(--cups-state-blue);
}
-td.cups-body {
- height: 100%;
- vertical-align: top;
+.status-pill.processing::before {
+ content: "";
+ display: inline-block;
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background-color: var(--cups-state-blue);
+ animation: pulseIndicator 1.8s infinite cubic-bezier(0.4, 0, 0.6, 1);
}
-td.trailer {
- background: #f0f0f0;
- border: solid thin #e0e0e0;
- color: #666666;
- font-size: 80%;
- padding: 5px;
+.status-pill.paused, .status-pill.held {
+ background-color: var(--cups-state-amber-bg);
+ border: 1px solid var(--cups-state-amber);
+ color: var(--cups-state-amber);
}
-td.trailer a {
- color: #666699;
+.status-pill.stopped, .status-pill.error, .status-pill.aborted {
+ background-color: var(--cups-state-coral-bg);
+ border: 1px solid var(--cups-state-coral);
+ color: var(--cups-state-coral);
}
-form {
- display: inline;
+/* ==========================================================================
+ 8. ESTADOS VAZIOS & DETALHES UNIX RAIZ
+ ========================================================================== */
+.empty-state-cell, .empty-state-box {
+ background-color: var(--cups-bg-surface);
+ border: 1px dashed var(--cups-border);
+ border-radius: var(--radius-md);
+ margin: var(--space-6) 0;
+ padding: var(--space-8) var(--space-4);
+ text-align: center;
}
-button, input[type=button], input[type=submit] {
- border: none;
- border-radius: 5px;
+.empty-ascii {
+ color: var(--cups-accent-green);
+ font-family: var(--font-mono);
+ font-size: var(--font-size-lg);
+ font-weight: 700;
+ letter-spacing: 2px;
+ margin-bottom: var(--space-2);
+}
+
+.empty-text {
+ color: var(--cups-text-secondary);
+ font-size: var(--font-size-sm);
+}
+
+/* Bloco CLI Copiável */
+.cli-block {
+ align-items: center;
+ background-color: var(--cups-bg-surface);
+ border: 1px solid var(--cups-border);
+ border-radius: var(--radius-sm);
+ display: flex;
+ font-family: var(--font-mono);
+ font-size: var(--font-size-sm);
+ justify-content: space-between;
+ margin: var(--space-2) 0;
+ padding: 8px 12px;
+ position: relative;
+}
+
+.cli-block .cmd-prompt {
+ color: var(--cups-accent-green);
+ font-weight: 700;
+ user-select: none;
+}
+
+.cli-block .cmd-comment {
+ color: var(--cups-text-muted);
+}
+
+.copy-btn {
+ background-color: var(--cups-bg-elevated);
+ border: 1px solid var(--cups-border);
+ border-radius: var(--radius-sm);
+ color: var(--cups-text-secondary);
cursor: pointer;
- font-family: inherit;
- font-size: 14px;
- font-weight: normal;
- padding: 4px 8px;
- text-decoration: none;
- white-space: nowrap;
- -moz-appearance: none;
- -webkit-appearance: none;
+ font-family: var(--font-sans);
+ font-size: var(--font-size-xs);
+ padding: 3px 8px;
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ transform: translateY(-50%);
+ transition: all 120ms ease;
+}
+
+.copy-btn:hover {
+ background-color: var(--cups-bg-hover);
+ border-color: var(--cups-accent-green);
+ color: var(--cups-text-primary);
+}
+
+.copy-btn.copied {
+ background-color: var(--cups-accent-green-bg);
+ border-color: var(--cups-accent-green);
+ color: var(--cups-accent-green);
+}
+
+/* ==========================================================================
+ 9. PAINEL DE DIAGNÓSTICO CONTEXTUAL (ROOT INTELLIGENCE)
+ ========================================================================== */
+.diagnostic-panel {
+ background-color: var(--cups-bg-surface);
+ border: 1px solid var(--cups-border);
+ border-radius: var(--radius-md);
+ margin: var(--space-6) 0;
+ overflow: hidden;
}
-button, input[type=button], input[type=file], input[type=submit] {
- background: #317BFF;
- color: #FFF;
+
+.diag-header {
+ align-items: center;
+ background-color: var(--cups-bg-elevated);
+ border-bottom: 1px solid var(--cups-border);
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-3);
+ justify-content: space-between;
+ padding: var(--space-3) var(--space-4);
}
-button:hover, input[type=button]:hover, input[type=file]:hover, input[type=submit]:hover {
- background: #65ABFF;
- color: #FFF;
+
+.diag-header-left {
+ align-items: center;
+ display: flex;
+ gap: var(--space-2);
}
-button:active, input[type=button]:active, input[type=file]:active, input[type=submit]:active {
- background: #317BFF;
- color: #FFF;
+
+.diag-title {
+ color: var(--cups-text-primary);
+ font-family: var(--font-sans);
+ font-size: var(--font-size-md);
+ font-weight: 600;
}
-input[type=search] {
- -webkit-appearance: textfield;
+
+.diag-badge-rule {
+ color: var(--cups-accent-green);
+ font-family: var(--font-mono);
+ font-size: var(--font-size-xs);
}
-input[type=search], input[type=text] {
- padding: 4px 8px;
+
+.diag-header-right {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
}
-select[size=1] {
- background: #ccc url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23000000' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px;
- color: black;
- padding: 4px 32px 4px 8px;
+
+.btn-diag-action {
+ background-color: var(--cups-bg-surface);
+ border: 1px solid var(--cups-border);
+ border-radius: var(--radius-sm);
+ color: var(--cups-text-secondary);
+ font-size: var(--font-size-xs);
+ padding: 4px 10px;
}
-select[size=1]:hover {
- background: #ddd url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23000000' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px;
+
+.btn-diag-action:hover {
+ background-color: var(--cups-bg-hover);
+ border-color: var(--cups-accent-green);
+ color: var(--cups-text-primary);
}
-textarea {
- width: 100%;
+
+.diag-body {
+ padding: var(--space-4);
}
-input[type="image"] {
- border: none;
- padding: 2pt;
- vertical-align: bottom;
+/* Grid de 4 Cards: Observado, Hipótese, Verificação, Ação */
+.diag-grid {
+ display: grid;
+ gap: var(--space-4);
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
-sub, sup {
- font-size: 50%;
+.diag-card {
+ background-color: var(--cups-bg-elevated);
+ border: 1px solid var(--cups-border);
+ border-radius: var(--radius-sm);
+ display: flex;
+ flex-direction: column;
+ padding: var(--space-4);
+ position: relative;
+ transition: transform var(--transition-normal), border-color var(--transition-normal), box-shadow var(--transition-normal);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
}
-tr.data, td.data, tr.data td {
- margin-top: 10pt;
- padding: 5pt;
- border-bottom: solid 1pt #999999;
+.diag-card:hover {
+ transform: translateY(-2px);
+ border-color: var(--cups-border-strong);
+ box-shadow: 0 8px 20px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05);
}
-tr.data th {
- border-bottom: solid 1pt #999999;
- padding-top: 10pt;
- padding-left: 5pt;
- text-align: left;
+.diag-card:has(.tag-obs), .diag-card.diag-card-obs { border-top: 2px solid var(--cups-state-blue); }
+.diag-card:has(.tag-hyp), .diag-card.diag-card-hyp { border-top: 2px solid var(--cups-state-amber); }
+.diag-card:has(.tag-ver), .diag-card.diag-card-ver { border-top: 2px solid #C5A4E8; }
+.diag-card:has(.tag-act), .diag-card.diag-card-act { border-top: 2px solid var(--cups-accent-green); }
+
+.card-tag {
+ border-radius: var(--radius-sm);
+ display: inline-block;
+ font-family: var(--font-mono);
+ font-size: var(--font-size-xs);
+ font-weight: 700;
+ letter-spacing: 0.05em;
+ margin-bottom: var(--space-2);
+ padding: 2px 6px;
+ text-transform: uppercase;
+ width: fit-content;
}
-div.table table {
- border: solid thin rgba(191,191,191,.3);
- border-collapse: collapse;
- border-spacing: 0;
- margin-left: auto;
- margin-right: auto;
+.tag-obs { background-color: var(--cups-state-blue-bg); color: var(--cups-state-blue); }
+.tag-hyp { background-color: var(--cups-state-amber-bg); color: var(--cups-state-amber); }
+.tag-ver { background-color: rgba(180, 150, 220, 0.15); color: #C5A4E8; }
+.tag-act { background-color: var(--cups-accent-green-bg); color: var(--cups-accent-green); }
+
+.card-content p {
+ color: var(--cups-text-primary);
+ font-size: var(--font-size-sm);
+ margin-bottom: var(--space-2);
}
-div.table caption {
- caption-side: top;
- font-size: 120%;
- font-style: italic;
- font-weight: bold;
- margin-left: auto;
- margin-right: auto;
+.card-meta {
+ color: var(--cups-text-muted);
+ font-family: var(--font-mono);
+ font-size: var(--font-size-xs);
+ margin-top: auto;
+ padding-top: var(--space-2);
+}
+
+.diag-msg-quote {
+ background-color: var(--cups-bg-surface);
+ border-left: 2px solid var(--cups-state-amber);
+ margin: var(--space-2) 0;
+ padding: var(--space-2);
+}
+
+/* Seção LLM Local */
+.diag-llm-section {
+ background-color: var(--cups-bg-elevated);
+ border: 1px solid var(--cups-border-subtle);
+ border-radius: var(--radius-sm);
+ margin-top: var(--space-4);
+ padding: var(--space-3) var(--space-4);
+}
+
+.llm-header {
+ align-items: center;
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-2);
+ justify-content: space-between;
+}
+
+.llm-status-indicator {
+ font-size: 10px;
+}
+.llm-status-indicator.status-configured { color: var(--cups-accent-green); }
+.llm-status-indicator.status-unconfigured { color: var(--cups-text-muted); }
+
+.llm-status-text {
+ color: var(--cups-text-secondary);
+ font-family: var(--font-mono);
+ font-size: var(--font-size-xs);
+}
+
+.llm-offline-note {
+ color: var(--cups-text-muted);
+ font-family: var(--font-mono);
+ font-size: var(--font-size-xs);
}
-div.table table td {
- background: white;
- border: solid thin rgba(191,191,191,.45);
- padding: 5pt 10pt 0;
+.llm-result-box {
+ border-top: 1px solid var(--cups-border);
+ margin-top: var(--space-3);
+ padding-top: var(--space-3);
+}
+
+/* ==========================================================================
+ 10. MODAIS & JANELAS DE DIÁLOGO
+ ========================================================================== */
+.cups-modal-overlay {
+ align-items: center;
+ background-color: rgba(0, 0, 0, 0.75);
+ bottom: 0;
+ display: none;
+ justify-content: center;
+ left: 0;
+ padding: var(--space-4);
+ position: fixed;
+ right: 0;
+ top: 0;
+ z-index: 2000;
+}
+
+.cups-modal-overlay.active {
+ display: flex;
+}
+
+.cups-modal-box {
+ background-color: var(--cups-bg-surface);
+ border: 1px solid var(--cups-border);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-elevated);
+ max-width: 600px;
+ overflow: hidden;
+ width: 100%;
}
-div.table table th {
- background: rgba(191,191,191,.15);
+.modal-header {
+ align-items: center;
+ background-color: var(--cups-bg-elevated);
+ border-bottom: 1px solid var(--cups-border);
+ display: flex;
+ gap: var(--space-2);
+ padding: var(--space-3) var(--space-4);
+}
+
+.modal-header h3 {
+ flex: 1;
+ font-size: var(--font-size-md);
+ margin: 0;
+}
+
+.modal-close-btn {
+ background: none;
border: none;
- border-bottom: solid thin rgba(191,191,191,.3);
+ color: var(--cups-text-secondary);
+ cursor: pointer;
+ font-size: var(--font-size-lg);
+ line-height: 1;
+ padding: 0 var(--space-1);
}
-div.figure table {
- margin-left: auto;
- margin-right: auto;
+.modal-close-btn:hover {
+ color: var(--cups-text-primary);
}
-div.figure caption {
- caption-side: bottom;
- font-size: 120%;
- font-style: italic;
- font-weight: bold;
- margin-left: auto;
- margin-right: auto;
+.modal-body {
+ padding: var(--space-4);
}
-th.label {
- text-align: right;
- vertical-align: top;
+.shortcuts-table {
+ width: 100%;
+ margin-bottom: var(--space-4);
}
-th.sublabel {
+.shortcuts-table th {
+ padding: 6px 12px;
text-align: right;
- font-weight: normal;
+ width: 30%;
}
-hr {
- border: solid thin;
+.shortcuts-table td {
+ color: var(--cups-text-secondary);
+ font-size: var(--font-size-sm);
+ padding: 6px 12px;
}
-span.info {
- background: black;
- border: thin solid black;
- color: white;
- font-size: 80%;
- font-style: italic;
- font-weight: bold;
- white-space: nowrap;
+.form-group {
+ margin-bottom: var(--space-4);
}
-h2 span.info, h3 span.info, h4 span.info {
- float: right;
- font-size: 100%;
+.form-group label {
+ display: block;
+ font-size: var(--font-size-sm);
+ margin-bottom: var(--space-1);
}
-.conflict {
- background: red;
- color: white;
+.form-group .form-help {
+ color: var(--cups-text-muted);
+ display: block;
+ font-size: var(--font-size-xs);
+ margin-top: var(--space-1);
}
-th.conflict {
- text-align: right;
+.modal-actions {
+ display: flex;
+ gap: var(--space-2);
+ justify-content: flex-end;
+ margin-top: var(--space-6);
}
-h1.title, h2.title, h3.title, .row .cups-body h2, .row .cups-body h3 {
- border-bottom: solid 2pt black;
+/* ==========================================================================
+ 10. ANIMAÇÕES & MICRO-INTERAÇÕES (UX/UI CONTEMPORÂNEO)
+ ========================================================================== */
+@keyframes unixSectionEnter {
+ from {
+ opacity: 0;
+ transform: translateY(8px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
}
-table.indent {
- margin-top: 2em;
- margin-left: auto;
- margin-right: auto;
- width: 90%;
+@keyframes pulseIndicator {
+ 0% {
+ transform: scale(0.9);
+ box-shadow: 0 0 0 0 rgba(159, 191, 203, 0.7);
+ }
+ 70% {
+ transform: scale(1);
+ box-shadow: 0 0 0 5px rgba(159, 191, 203, 0);
+ }
+ 100% {
+ transform: scale(0.9);
+ box-shadow: 0 0 0 0 rgba(159, 191, 203, 0);
+ }
}
-table.indent {
- border-collapse: collapse;
+@keyframes toastSlideUp {
+ from {
+ opacity: 0;
+ transform: translateY(12px) scale(0.96);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
}
-table.indent td, table.indent th {
- padding: 0;
+/* Entrada animada das sessões e tabelas */
+.unix-banner {
+ animation: unixSectionEnter 240ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
-table.list {
- border-collapse: collapse;
- margin-left: auto;
- margin-right: auto;
- width: 90%;
+.table-container, table.list {
+ animation: unixSectionEnter 280ms cubic-bezier(0.16, 1, 0.3, 1) 40ms both;
}
-table.list th {
- background: white;
- border-bottom: solid thin #cccccc;
- border-right: 2px solid rgba(191,191,191,.45);
- color: #444444;
- padding: 5px 10px 5px 12px;
- text-align: left;
- vertical-align: bottom;
- white-space: nowrap;
+.diag-panel {
+ animation: unixSectionEnter 320ms cubic-bezier(0.16, 1, 0.3, 1) 80ms both;
}
-table.list th a {
- color: #4444cc;
+.row .thirds, .row .halves {
+ animation: unixSectionEnter 280ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
-table.list td {
- border-bottom: solid thin rgba(191,191,191,.45);
- padding: 5px 2px 5px 10px;
+.quick-action-bar form, .quick-action-bar a {
+ transition: transform var(--transition-fast);
}
-table.list tr:nth-child(even) {
- background: rgba(191,191,191,0.15);
+.quick-action-bar form:hover, .quick-action-bar a:hover {
+ transform: translateY(-1px);
}
-table.list tr:nth-child(odd) {
- background: rgba(191,191,191,0.3);
+/* Toast de Notificação de Ação Concluída */
+.cups-toast {
+ align-items: center;
+ animation: toastSlideUp 220ms cubic-bezier(0.16, 1, 0.3, 1) both;
+ background-color: var(--cups-bg-elevated);
+ border: 1px solid var(--cups-accent-green);
+ border-radius: var(--radius-sm);
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.7), 0 0 12px rgba(168, 198, 134, 0.2);
+ color: var(--cups-text-primary);
+ display: flex;
+ font-family: var(--font-mono);
+ font-size: var(--font-size-xs);
+ gap: 8px;
+ padding: 8px 16px;
+ position: fixed;
+ bottom: 24px;
+ right: 24px;
+ z-index: 9999;
}
-div.sidebar {
- float: right;
- min-width: 25%;
- margin-left: 10px;
- max-width: 33%;
+.cups-toast-icon {
+ color: var(--cups-accent-green);
+ font-weight: bold;
}
-div.sidebar p.l0 {
- margin-bottom: 0;
- margin-left: 0;
- margin-right: 0;
- margin-top: 12pt;
+/* ==========================================================================
+ 11. COMPONENTES AVANÇADOS UX/UI & MOTION (Root Intelligence Hybrid)
+ ========================================================================== */
+
+/* Regras de Visibilidade de Modo */
+html.mode-beginner .advanced-only {
+ display: none !important;
}
-div.sidebar p.l1 {
- margin-bottom: 0;
- margin-left: 36pt;
- margin-right: 0;
- margin-top: 0;
- text-indent: -18pt;
+html.mode-advanced .beginner-only {
+ display: none !important;
}
-div.sidebar p.l2 {
- font-style: italic;
- margin-bottom: 0;
- margin-left: 54pt;
- margin-right: 0;
- margin-top: 0;
- text-indent: -18pt;
+/* Efeito de Vidro e Borda Iluminada nos Cards e Seções */
+.home-hero, .onboarding-banner, .server-actions-bar, .cli-quickref, .server-alerts-box, .diag-card {
+ border: 1px solid var(--cups-border-subtle);
+ transition: transform var(--transition-spring), border-color var(--transition-normal), box-shadow var(--transition-normal);
}
-table.inset {
- background: #f4f4f4;
- border: thin solid rgba(191,191,191,0.5);
- margin-top: 1em;
- padding: 0;
- width: 100%;
- /* these are not implemented by all browsers, but that's ok */
- border-radius: 5px;
- -moz-border-radius: 5px;
+.home-hero:hover, .onboarding-banner:hover, .server-actions-bar:hover, .cli-quickref:hover, .diag-card:hover {
+ border-color: rgba(168, 198, 134, 0.35);
+ box-shadow: 0 8px 28px rgba(0, 0, 0, 0.4), 0 0 16px rgba(168, 198, 134, 0.12);
+ transform: translateY(-1px);
}
-table.inset caption {
- caption-side: top;
- color: #666666;
- font-size: 80%;
- margin-left: 10px;
- margin-bottom: 2px;
- text-align: left;
+/* Banner de Primeiros Passos e Checklist de Integração Linux */
+.onboarding-banner {
+ background: linear-gradient(145deg, rgba(24, 28, 25, 0.95), rgba(20, 24, 21, 0.9));
+ backdrop-filter: blur(14px);
+ -webkit-backdrop-filter: blur(14px);
+ border: 1px solid rgba(168, 198, 134, 0.25);
+ border-radius: var(--radius-lg);
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45), 0 0 20px rgba(168, 198, 134, 0.08);
+ margin-bottom: var(--space-6);
+ padding: var(--space-6);
+ position: relative;
+ overflow: hidden;
}
-table.inset td {
- padding: 2px 5px;
+.onboarding-banner::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 2px;
+ background: linear-gradient(90deg, transparent, var(--cups-accent-green), transparent);
}
-dt {
- margin-left: 36pt;
- margin-top: 12pt;
+.onboarding-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: var(--space-4);
+ margin-bottom: var(--space-4);
}
-dd {
- margin-left: 54pt;
+.onboarding-title-area {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
}
-dl.category dt {
- font-weight: bold;
+.onboarding-icon {
+ font-size: 28px;
+ line-height: 1;
}
-p.summary {
- margin-left: 36pt;
+.onboarding-title {
+ color: var(--cups-text-primary);
+ font-size: var(--font-size-lg);
+ font-weight: 700;
+ margin: 0;
}
-div.summary table {
- border: solid thin #999999;
- border-collapse: collapse;
- border-spacing: 0;
- margin: 10px;
+.onboarding-subtitle {
+ color: var(--cups-text-muted);
+ font-size: var(--font-size-sm);
+ margin: 2px 0 0 0;
}
-div.summary table td, div.summary table th {
- background: white;
- border: solid thin #999999;
- border-spacing: 0;
- padding: 5px;
- text-align: left;
- vertical-align: top;
+.btn-tour-launch {
+ align-items: center;
+ background: var(--cups-accent-green-bg);
+ border: 1px solid var(--cups-accent-green);
+ border-radius: var(--radius-md);
+ color: var(--cups-accent-green);
+ cursor: pointer;
+ display: inline-flex;
+ font-family: var(--font-sans);
+ font-size: var(--font-size-sm);
+ font-weight: 600;
+ gap: 8px;
+ padding: 8px 16px;
+ transition: all var(--transition-normal);
+}
+
+.btn-tour-launch:hover {
+ background: var(--cups-accent-green);
+ color: var(--cups-bg-main);
+ box-shadow: 0 0 16px rgba(168, 198, 134, 0.4);
+ transform: translateY(-1px);
+}
+
+.btn-tour-launch:active {
+ transform: scale(0.97);
}
-div.summary table thead th {
- background: #f0f0f0;
+.onboarding-progress-wrapper {
+ display: flex;
+ align-items: center;
+ gap: var(--space-4);
+ margin-bottom: var(--space-4);
}
-div.tabs {
- height: 480px;
+.onboarding-progress-bar {
+ flex: 1;
+ height: 8px;
+ background: rgba(255, 255, 255, 0.08);
+ border-radius: 4px;
overflow: hidden;
}
-div.tab {
- float: left;
+.onboarding-progress-fill {
height: 100%;
- overflow-y: auto;
- width: 100%;
+ background: linear-gradient(90deg, #A8C686, #BCD89A);
+ border-radius: 4px;
+ transition: width 350ms cubic-bezier(0.16, 1, 0.3, 1);
+ box-shadow: 0 0 8px rgba(168, 198, 134, 0.5);
}
-/* API documentation styles... */
-div.cups-body h1 {
+.onboarding-progress-text {
+ color: var(--cups-accent-green);
+ font-family: var(--font-mono);
+ font-size: var(--font-size-xs);
+ font-weight: 600;
+ white-space: nowrap;
}
-div.cups-body h2 {
+
+.onboarding-checklist {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: var(--space-3);
}
-div.cups-body h3 {
+
+.check-item {
+ align-items: flex-start;
+ background: rgba(16, 18, 17, 0.6);
+ border: 1px solid var(--cups-border-subtle);
+ border-radius: var(--radius-md);
+ cursor: pointer;
+ display: flex;
+ gap: var(--space-3);
+ padding: var(--space-3);
+ transition: all var(--transition-fast);
+ user-select: none;
}
-div.cups-body h4 {
+
+.check-item:hover {
+ background: var(--cups-bg-hover);
+ border-color: rgba(168, 198, 134, 0.3);
}
-div.cups-body h5 {
+
+.check-item input[type="checkbox"] {
+ display: none;
}
-div.contents {
+
+.check-box {
+ width: 20px;
+ height: 20px;
+ border-radius: var(--radius-sm);
+ border: 2px solid var(--cups-border-strong);
+ background: var(--cups-bg-input);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 13px;
+ font-weight: bold;
+ color: var(--cups-bg-main);
+ flex-shrink: 0;
+ margin-top: 2px;
+ transition: all var(--transition-fast);
}
-div.contents h1 {
+
+.check-item.done .check-box {
+ background: var(--cups-accent-green);
+ border-color: var(--cups-accent-green);
+ box-shadow: 0 0 8px rgba(168, 198, 134, 0.4);
}
-div.contents h2 {
+
+.check-content strong {
+ display: block;
+ font-size: var(--font-size-sm);
+ color: var(--cups-text-primary);
+ margin-bottom: 2px;
}
-div.contents ul.contents {
+
+.check-content small {
+ display: block;
+ font-size: var(--font-size-xs);
+ color: var(--cups-text-muted);
+ line-height: 1.4;
}
-div.contents ul.contents li ul {
- display: none;
+
+.check-item.done .check-content strong {
+ color: var(--cups-text-secondary);
}
-.class {
- border-bottom: solid 2px gray;
+.onboarding-footer {
+ border-top: 1px solid var(--cups-border-subtle);
+ margin-top: var(--space-4);
+ padding-top: var(--space-3);
}
-.constants {
+
+.onboarding-hint {
+ color: var(--cups-text-muted);
+ font-size: var(--font-size-xs);
}
-.description {
- margin-top: 0.5em;
+
+/* Barra de Telemetria Avançada */
+.advanced-telemetry-bar {
+ background: var(--cups-bg-surface);
+ border: 1px solid var(--cups-border);
+ border-left: 3px solid var(--cups-state-amber);
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-subtle);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: var(--space-4);
+ margin-bottom: var(--space-6);
+ padding: var(--space-3) var(--space-4);
}
-.discussion {
+
+.telemetry-badge {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ color: var(--cups-state-amber);
+ font-family: var(--font-mono);
+ font-size: var(--font-size-xs);
+ font-weight: 600;
}
-.enumeration {
- border-bottom: solid 2px gray;
+
+.pulse-indicator {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--cups-state-amber);
+ box-shadow: 0 0 8px var(--cups-state-amber);
+ animation: pulseAmber 2s infinite;
}
-.function {
- border-bottom: solid 2px gray;
- margin-bottom: 0;
+
+@keyframes pulseAmber {
+ 0% { transform: scale(0.95); opacity: 0.8; }
+ 50% { transform: scale(1.15); opacity: 1; box-shadow: 0 0 12px var(--cups-state-amber); }
+ 100% { transform: scale(0.95); opacity: 0.8; }
}
-.members {
+
+.telemetry-metrics {
+ display: flex;
+ align-items: center;
+ gap: var(--space-4);
+ flex-wrap: wrap;
}
-.method {
+
+.metric-item {
+ font-size: var(--font-size-xs);
+ color: var(--cups-text-muted);
}
-.parameters {
+
+.metric-value {
+ color: var(--cups-text-code);
+ background: rgba(255, 255, 255, 0.05);
+ padding: 2px 6px;
+ border-radius: 3px;
}
-.returnvalue {
+
+/* Modal e Paleta de Comandos (Ctrl+K) */
+.cmd-palette-backdrop {
+ align-items: flex-start;
+ animation: cmdFadeIn 150ms ease-out;
+ background: rgba(8, 10, 9, 0.82);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ display: flex;
+ inset: 0;
+ justify-content: center;
+ padding: 10vh var(--space-4) var(--space-6);
+ position: fixed;
+ z-index: 10000;
}
-.struct {
- border-bottom: solid 2px gray;
+
+@keyframes cmdFadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
}
-.typedef {
- border-bottom: solid 2px gray;
+
+.cmd-palette-box {
+ animation: cmdSlideDown 180ms cubic-bezier(0.16, 1, 0.3, 1);
+ background: rgba(20, 24, 21, 0.96);
+ border: 1px solid var(--cups-accent-green);
+ border-radius: var(--radius-lg);
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.7), 0 0 25px rgba(168, 198, 134, 0.22);
+ display: flex;
+ flex-direction: column;
+ max-height: 80vh;
+ max-width: 660px;
+ margin: 0 auto;
+ overflow: hidden;
+ width: calc(100% - 24px);
}
-.union {
- border-bottom: solid 2px gray;
+
+@keyframes cmdSlideDown {
+ from { opacity: 0; transform: translateY(-16px) scale(0.98); }
+ to { opacity: 1; transform: translateY(0) scale(1); }
}
-.variable {
+
+.cmd-palette-input-wrap {
+ align-items: center;
+ border-bottom: 1px solid var(--cups-border);
+ display: flex;
+ padding: var(--space-3) var(--space-4);
+ gap: var(--space-3);
+ position: relative;
}
-h1, h2, h3, h4, h5, h6 {
- page-break-inside: avoid;
+
+.cmd-palette-prompt {
+ color: var(--cups-accent-green);
+ font-family: var(--font-mono);
+ font-size: var(--font-size-lg);
+ font-weight: 700;
+ user-select: none;
}
-blockquote {
- page-break-inside: avoid;
+
+.cmd-palette-input {
+ background: transparent !important;
+ border: 0 !important;
+ box-shadow: none !important;
+ color: var(--cups-text-primary) !important;
+ flex: 1;
+ font-family: var(--font-sans);
+ font-size: var(--font-size-md) !important;
+ outline: none !important;
+ padding: 4px 0 !important;
+ width: 100%;
}
-p.code, pre, ul.code li {
- border-left: thin dotted gray;
- background: rgba(191,191,191,0.1);
+
+.cmd-palette-input::placeholder {
+ color: var(--cups-text-muted);
}
-p code, li code {
- background: rgba(191,191,191,0.3);
+
+.cmd-palette-close {
+ background: transparent;
+ border: 1px solid var(--cups-border);
+ border-radius: var(--radius-sm);
+ color: var(--cups-text-muted);
+ cursor: pointer;
+ font-family: var(--font-mono);
+ font-size: var(--font-size-xs);
+ padding: 3px 8px;
+ transition: all var(--transition-fast);
}
-p code, li code, p.code, pre, ul.code li {
- hyphens: manual;
- margin: 18pt 0;
- -webkit-hyphens: manual;
- page-break-inside: avoid;
+
+.cmd-palette-close:hover {
+ background: var(--cups-bg-hover);
+ color: var(--cups-text-primary);
}
-p.code, pre, ul.code li {
- padding: 10px;
+
+.cmd-palette-list {
+ list-style: none;
+ margin: 0;
+ overflow-y: auto;
+ padding: var(--space-2) 0;
+ max-height: 420px;
}
-p code, li code {
- padding: 2px 5px;
+
+.cmd-palette-group-title {
+ color: var(--cups-accent-green);
+ font-family: var(--font-mono);
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.05em;
+ padding: 8px var(--space-4) 4px;
+ text-transform: uppercase;
+}
+
+.cmd-palette-item {
+ align-items: center;
+ cursor: pointer;
+ display: flex;
+ gap: var(--space-3);
+ padding: 10px var(--space-4);
+ transition: background var(--transition-fast), border-color var(--transition-fast);
+ border-left: 3px solid transparent;
+}
+
+.cmd-palette-item:hover, .cmd-palette-item.selected {
+ background: rgba(168, 198, 134, 0.12);
+ border-left-color: var(--cups-accent-green);
}
-span.info {
- background: black;
- border: solid thin black;
- color: white;
- font-size: 80%;
- font-style: italic;
+
+.cmd-item-icon {
+ color: var(--cups-accent-green);
+ font-family: var(--font-mono);
+ font-size: var(--font-size-sm);
font-weight: bold;
+ width: 20px;
+ text-align: center;
+}
+
+.cmd-item-details {
+ flex: 1;
+ min-width: 0;
+}
+
+.cmd-item-title {
+ color: var(--cups-text-primary);
+ font-size: var(--font-size-sm);
+ font-weight: 500;
+}
+
+.cmd-item-sub {
+ color: var(--cups-text-muted);
+ font-size: var(--font-size-xs);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.cmd-item-badge {
+ background: rgba(255, 255, 255, 0.06);
+ border: 1px solid var(--cups-border-subtle);
+ border-radius: var(--radius-sm);
+ color: var(--cups-text-secondary);
+ font-family: var(--font-mono);
+ font-size: 11px;
+ padding: 2px 6px;
white-space: nowrap;
}
-h3 span.info, h4 span.info {
- border-top-left-radius: 10px;
- border-top-right-radius: 10px;
- float: right;
- padding: 3px 6px;
+
+.cmd-palette-empty {
+ color: var(--cups-text-muted);
+ font-size: var(--font-size-sm);
+ padding: var(--space-6);
+ text-align: center;
+}
+
+.cmd-palette-footer {
+ align-items: center;
+ border-top: 1px solid var(--cups-border);
+ display: flex;
+ justify-content: space-between;
+ padding: 8px var(--space-4);
+ background: var(--cups-bg-input);
+ font-size: var(--font-size-xs);
+ color: var(--cups-text-muted);
+}
+
+.cmd-palette-footer kbd {
+ background: rgba(255, 255, 255, 0.08);
+ border: 1px solid var(--cups-border-subtle);
+ border-radius: 3px;
+ color: var(--cups-text-secondary);
+ font-family: var(--font-mono);
+ font-size: 10px;
+ padding: 1px 4px;
+ margin-right: 2px;
+}
+
+/* Modal do Tour Guiado */
+.tour-modal-backdrop {
+ align-items: center;
+ animation: cmdFadeIn 150ms ease-out;
+ background: rgba(8, 10, 9, 0.85);
+ backdrop-filter: blur(14px);
+ -webkit-backdrop-filter: blur(14px);
+ display: flex;
+ inset: 0;
+ justify-content: center;
+ padding: var(--space-4);
+ position: fixed;
+ z-index: 10001;
+}
+
+.tour-modal-box {
+ animation: cmdSlideDown 200ms cubic-bezier(0.16, 1, 0.3, 1);
+ background: linear-gradient(145deg, #181C19, #131614);
+ border: 1px solid var(--cups-accent-green);
+ border-radius: var(--radius-lg);
+ box-shadow: 0 24px 60px rgba(0, 0, 0, 0.75), 0 0 30px rgba(168, 198, 134, 0.2);
+ max-width: 580px;
+ overflow: hidden;
+ position: relative;
+ width: 100%;
+}
+
+.tour-modal-header {
+ align-items: center;
+ border-bottom: 1px solid var(--cups-border);
+ display: flex;
+ justify-content: space-between;
+ padding: var(--space-4);
}
-ul.code, ul.contents, ul.subcontents {
- list-style-type: none;
+
+.tour-modal-title {
+ color: var(--cups-accent-green);
+ font-size: var(--font-size-md);
+ font-weight: 700;
margin: 0;
- padding-left: 0;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.tour-step-body {
+ padding: var(--space-6) var(--space-4);
+}
+
+.tour-step-badge {
+ background: var(--cups-accent-green-bg);
+ border: 1px solid rgba(168, 198, 134, 0.4);
+ border-radius: 20px;
+ color: var(--cups-accent-green);
+ display: inline-block;
+ font-family: var(--font-mono);
+ font-size: 11px;
+ font-weight: 600;
+ margin-bottom: var(--space-2);
+ padding: 2px 10px;
+}
+
+.tour-step-heading {
+ color: var(--cups-text-primary);
+ font-size: var(--font-size-lg);
+ font-weight: 700;
+ margin: 0 0 var(--space-2) 0;
}
-ul.code li {
+
+.tour-step-desc {
+ color: var(--cups-text-secondary);
+ font-size: var(--font-size-sm);
+ line-height: 1.6;
+ margin: 0 0 var(--space-4) 0;
+}
+
+.tour-features-list {
+ background: var(--cups-bg-input);
+ border: 1px solid var(--cups-border-subtle);
+ border-radius: var(--radius-md);
+ list-style: none;
margin: 0;
+ padding: var(--space-3) var(--space-4);
}
-ul.contents > li {
- margin-top: 1em;
+
+.tour-features-list li {
+ color: var(--cups-text-secondary);
+ font-size: var(--font-size-sm);
+ margin-bottom: 6px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
}
-ul.contents li ul.code, ul.contents li ul.subcontents {
- padding-left: 2em;
+
+.tour-features-list li:last-child {
+ margin-bottom: 0;
}
-/* iPhone/iPod touch overrides */
-@media only screen and (min-device-width: 320px) and (max-device-width: 480px),
- only screen and (min-device-width: 320px) and (max-device-width: 568px) {
- .mobile {
- display: inherit;
- }
- .no-mobile {
- display: none;
- }
+.tour-modal-footer {
+ align-items: center;
+ background: var(--cups-bg-input);
+ border-top: 1px solid var(--cups-border);
+ display: flex;
+ justify-content: space-between;
+ padding: var(--space-3) var(--space-4);
+}
- .cups-header {
- margin: 0;
- position: relative;
- }
- .cups-header ul li {
- float: none;
- }
+.tour-dots {
+ display: flex;
+ gap: 6px;
+}
- .cups-body {
- padding: 0;
- }
+.tour-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--cups-border);
+ transition: all var(--transition-fast);
+}
- .cups-footer {
- font-size: 10px;
- height: auto;
- position: relative;
- }
+.tour-dot.active {
+ background: var(--cups-accent-green);
+ box-shadow: 0 0 6px var(--cups-accent-green);
+ width: 16px;
+ border-radius: 4px;
+}
- .row .thirds, .row .halves {
- float: none;
- margin: 0;
- width: 100%;
- }
+.tour-nav-buttons {
+ display: flex;
+ gap: var(--space-2);
+}
+
+.tour-btn {
+ background: var(--cups-bg-surface);
+ border: 1px solid var(--cups-border);
+ border-radius: var(--radius-sm);
+ color: var(--cups-text-primary);
+ cursor: pointer;
+ font-family: var(--font-sans);
+ font-size: var(--font-size-sm);
+ padding: 6px 14px;
+ transition: all var(--transition-fast);
+}
+
+.tour-btn:hover {
+ background: var(--cups-bg-hover);
+ border-color: var(--cups-text-muted);
+}
+
+.tour-btn-primary {
+ background: var(--cups-accent-green);
+ border-color: var(--cups-accent-green);
+ color: var(--cups-bg-main);
+ font-weight: 600;
+}
+
+.tour-btn-primary:hover {
+ background: var(--cups-accent-green-hover);
+ box-shadow: 0 0 10px rgba(168, 198, 134, 0.4);
+}
+
+/* ==========================================================================
+ 12. RESPONSIVIDADE E ACESSIBILIDADE
+ ========================================================================== */
- div.sidebar {
+/* Rolagem Horizontal Limpa em Tabelas Grandes */
+.table-container {
+ margin: var(--space-4) 0;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ width: 100%;
+}
+
+@media (max-width: 900px) {
+ .row .thirds, .row .halves {
float: none;
margin-left: 0;
- max-width: 100%;
- min-width: 100%;
width: 100%;
+ margin-bottom: var(--space-6);
}
- blockquote {
- margin: 0;
- }
-
- p.example {
- margin-left: 0;
+ .cups-header ul {
+ padding: var(--space-2) var(--space-3);
}
- pre.command, pre.example, pre.man {
- margin-left: 0;
- white-space: pre-wrap;
+ .cups-header a {
+ padding: 8px 10px !important;
}
}
-/* iPad overrides */
-@media only screen and (min-device-width: 768px) and (max-device-width: 1024px) {
- .mobile {
- display: inherit;
- }
- .no-mobile {
- display: none;
+@media (max-width: 768px) {
+ .cups-header {
+ position: relative;
}
-}
-/* Dark mode overrides */
-@media (prefers-color-scheme: dark) {
- body {
- background: black;
- color: #ccc;
+ .cups-header ul {
+ flex-wrap: wrap;
+ gap: 4px;
+ padding: var(--space-2) var(--space-3);
+ min-height: auto;
}
- a:link, a:visited, table.list th a {
- color: #4C80BF;
+
+ .cups-header a {
+ padding: 6px 8px !important;
+ font-size: var(--font-size-xs) !important;
}
- a:link:hover, a:visited:hover, a:active, table.list th a {
- color: #65ABFF;
+
+ .cups-header ul li.right {
+ margin-left: 0;
+ width: 100%;
+ margin-top: var(--space-2);
+ justify-content: space-between;
}
- hr {
- color: #666;
+
+ .cups-header input[type="search"] {
+ width: 140px;
}
- table.inset {
- background: #1d1d1d;
+
+ .cups-body {
+ margin-top: 0 !important;
+ padding: var(--space-4) var(--space-3) !important;
}
- h2.title, h3.title, .row .cups-body h2, .row .cups-body h3 {
- border-bottom: solid 2pt #ccc;
+
+ .diag-grid {
+ grid-template-columns: 1fr;
}
- div.table table td, table.list th {
- background: black;
+}
+
+@media (max-width: 480px) {
+ .cups-header ul li.right {
+ flex-wrap: wrap;
+ gap: var(--space-2);
}
- table.list th {
- color: #666;
+
+ .cups-header input[type="search"] {
+ width: 100%;
}
- button, input[type=button], input[type=file], input[type=submit] {
- background: #255CBF;
- color: #DDD;
+
+ .cups-body {
+ padding: var(--space-3) var(--space-2) !important;
}
- button:hover, input[type=button]:hover, input[type=file]:hover, input[type=submit]:hover {
- background: #4C80BF;
- color: #EEE;
+
+ .cmd-palette-backdrop {
+ padding: 4vh 8px var(--space-4);
}
- button:active, input[type=button]:active, input[type=file]:active, input[type=submit]:active {
- background: #255CBF;
- color: #DDD;
+
+ .cmd-palette-box {
+ width: 100%;
+ max-height: 88vh;
}
- input[type=search], input[type=text], textarea {
- background: #333;
- color: #ccc;
+
+ .cmd-palette-footer {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 4px;
+ font-size: 11px;
}
- select[size=1] {
- background: #333 url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23CCCCCC' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px;
- color: #ccc;
- padding: 4px 32px 4px 8px;
+
+ .tour-modal-box {
+ max-width: 96%;
+ margin: 0 auto;
}
- select[size=1]:hover {
- background: #444 url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23DDDDDD' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px;
+}
+
+/* Respeito estrito a preferências de movimento reduzido */
+@media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ scroll-behavior: auto !important;
}
}
diff --git a/doc/cups.js b/doc/cups.js
new file mode 100644
index 0000000000..35f86082db
--- /dev/null
+++ b/doc/cups.js
@@ -0,0 +1,1475 @@
+/**
+ * CUPS - Root Intelligence Interface Enhancements (Progressive Enhancement)
+ * Unix Contemporâneo: Clareza, precisão e autonomia local.
+ *
+ * Funcionalidades 100% progressivas (a interface funciona normalmente sem JS):
+ * 1. Cópia rápida de comandos CLI com feedback visual
+ * 2. Filtragem instantânea de tabelas (impressoras e trabalhos)
+ * 3. Atalhos de teclado estilo Unix (/ para busca, ? para ajuda, Esc para fechar)
+ * 4. Confirmação explícita de ações destrutivas (com alvo declarado)
+ * 5. Assistência de Diagnóstico Local (regras determinísticas + conector opcional offline)
+ * 6. Higienização e exportação de resumo técnico para suporte
+ */
+
+(function() {
+ 'use strict';
+
+ // Configurações e estado do diagnóstico local
+ const STORAGE_KEY_LLM = 'cups_root_intelligence_llm';
+ const DEFAULT_LLM_CONFIG = {
+ enabled: false,
+ endpoint: 'http://localhost:11434/api/generate',
+ model: 'llama3.2',
+ timeoutMs: 15000
+ };
+
+ /**
+ * Base de Conhecimento Determinística Local (Heurísticas Unix CUPS)
+ * Regras determinísticas oficiais para diagnóstico de filas e erros
+ */
+ const HEURISTIC_RULES = [
+ {
+ id: 'ribbon_media_out',
+ match: function(state, msg, uri) {
+ const text = (state + ' ' + msg).toLowerCase();
+ return text.includes('fita') || text.includes('ribbon') || text.includes('papel') || text.includes('paper') || text.includes('suprimento');
+ },
+ diagnostic: {
+ pt: {
+ observed: 'Fila interrompida ou pausada por alerta de suprimento (papel, fita térmica ou etiquetas).',
+ origin: 'Sensor de mídia da impressora / Driver de comunicação',
+ hypothesis: 'O sensor mecânico ou óptico detectou fim de rolo/bandeja vazia, tampa aberta ou mídia desalinhada.',
+ verification: 'Inspecione visualmente a bandeja de alimentação e o painel frontal da impressora física.',
+ suggestedAction: 'Abasteça a mídia correta, feche a trava da cabeça térmica e execute o comando para reativar a fila.',
+ suggestedCommand: 'cupsenable {queue}',
+ commandDesc: 'Reativa o processamento de trabalhos na fila especificada.',
+ permission: 'Requer privilégios de administração (membro do grupo lpadmin ou root)'
+ },
+ en: {
+ observed: 'Queue stopped or paused due to supply alert (paper, ribbon, or labels).',
+ origin: 'Printer media sensor / Communication driver',
+ hypothesis: 'Mechanical or optical sensor detected empty tray/roll, open cover, or misaligned media.',
+ verification: 'Visually inspect the physical printer feed tray and front status indicator LEDs.',
+ suggestedAction: 'Reload the appropriate media, secure the print head latch, and resume the queue.',
+ suggestedCommand: 'cupsenable {queue}',
+ commandDesc: 'Resumes processing of jobs on the specified queue.',
+ permission: 'Requires administrative privileges (lpadmin group or root)'
+ }
+ }
+ },
+ {
+ id: 'paused_stopped_state',
+ match: function(state, msg, uri) {
+ return state === '5' || (state && state.toLowerCase().includes('pausada')) || (state && state.toLowerCase().includes('paused'));
+ },
+ diagnostic: {
+ pt: {
+ observed: 'A fila de impressão está atualmente no estado Pausada (State 5).',
+ origin: 'CUPS Scheduler (cupsd) ou comando administrativo manual',
+ hypothesis: 'A fila foi interrompida manualmente por um administrador ou o CUPS a pausou automaticamente após erro repetido.',
+ verification: 'Verifique se há trabalhos travados na fila com `lpstat -o {queue}` e consulte o log de erros.',
+ suggestedAction: 'Se não houver problemas pendentes no dispositivo físico, reative a fila para retomar as impressões.',
+ suggestedCommand: 'cupsenable {queue}',
+ commandDesc: 'Remove a pausa da fila e retoma o envio de trabalhos pendentes.',
+ permission: 'Requer privilégios de administração (lpadmin ou sudo)'
+ },
+ en: {
+ observed: 'Print queue is currently in Paused state (State 5).',
+ origin: 'CUPS Scheduler (cupsd) or manual administrative action',
+ hypothesis: 'Queue was manually stopped by an administrator or automatically paused by cupsd after an error.',
+ verification: 'Check for stuck jobs with `lpstat -o {queue}` and inspect error log.',
+ suggestedAction: 'If the physical device has no hardware issues, resume the queue.',
+ suggestedCommand: 'cupsenable {queue}',
+ commandDesc: 'Clears the queue pause and resumes pending print jobs.',
+ permission: 'Requires administrative privileges (lpadmin or sudo)'
+ }
+ }
+ },
+ {
+ id: 'backend_timeout',
+ match: function(state, msg, uri) {
+ const text = (msg + ' ' + uri).toLowerCase();
+ return text.includes('timeout') || text.includes('timed out') || text.includes('host unreachable') || text.includes('falha no backend') || text.includes('connection refused');
+ },
+ diagnostic: {
+ pt: {
+ observed: 'Falha de comunicação ou timeout de conexão com o dispositivo de impressão.',
+ origin: 'Backend do CUPS (socket, ipp ou usb)',
+ hypothesis: 'O endereço de rede da impressora não está acessível (cabo desconectado, IP alterado por DHCP, ou porta bloqueada por firewall).',
+ verification: 'Teste a rota de rede com `ping` ou `nc -zv 9100`.',
+ suggestedAction: 'Confirme o IP fixo no painel da impressora e, se necessário, altere o URI da conexão via painel administrativo.',
+ suggestedCommand: 'lpadmin -p {queue} -v socket://:9100',
+ commandDesc: 'Atualiza o endereço de conexão física/rede da fila.',
+ permission: 'Requer permissão de administração do CUPS'
+ },
+ en: {
+ observed: 'Communication failure or connection timeout with the printing device.',
+ origin: 'CUPS Backend (socket, ipp, or usb)',
+ hypothesis: 'The printer network address is unreachable (cable unplugged, IP changed by DHCP, or port blocked).',
+ verification: 'Test network route using `ping` or `nc -zv 9100`.',
+ suggestedAction: 'Verify the static IP on the device panel and update the printer URI if necessary.',
+ suggestedCommand: 'lpadmin -p {queue} -v socket://:9100',
+ commandDesc: 'Updates the device communication URI for the queue.',
+ permission: 'Requires CUPS administration privileges'
+ }
+ }
+ },
+ {
+ id: 'rejecting_jobs',
+ match: function(state, msg, uri, meta) {
+ return meta && meta.accepting === '0';
+ },
+ diagnostic: {
+ pt: {
+ observed: 'A fila está rejeitando novos trabalhos de impressão.',
+ origin: 'Configuração da fila no scheduler CUPS',
+ hypothesis: 'A fila foi configurada para manutenção ou desativação temporária usando cupsreject.',
+ verification: 'Confirme o status de aceitação executando `lpstat -a {queue}`.',
+ suggestedAction: 'Instrua o scheduler a aceitar novos trabalhos novamente usando cupsaccept.',
+ suggestedCommand: 'cupsaccept {queue}',
+ commandDesc: 'Configura a fila para voltar a aceitar o envio de novos documentos.',
+ permission: 'Requer privilégios de administração do CUPS'
+ },
+ en: {
+ observed: 'The queue is currently rejecting new print jobs.',
+ origin: 'Queue configuration in CUPS scheduler',
+ hypothesis: 'Queue was set to reject jobs during scheduled maintenance or decommissioning using cupsreject.',
+ verification: 'Verify acceptance status using `lpstat -a {queue}`.',
+ suggestedAction: 'Instruct the scheduler to accept new jobs again using cupsaccept.',
+ suggestedCommand: 'cupsaccept {queue}',
+ commandDesc: 'Sets the queue to accept incoming print documents.',
+ permission: 'Requires CUPS administrative privileges'
+ }
+ }
+ },
+ {
+ id: 'default_idle_operational',
+ match: function(state, msg, uri) {
+ return state === '3' || (state && state.toLowerCase().includes('ociosa')) || (state && state.toLowerCase().includes('idle'));
+ },
+ diagnostic: {
+ pt: {
+ observed: 'A fila está ociosa e apta a receber trabalhos (State 3 - Idle).',
+ origin: 'CUPS Scheduler (cupsd)',
+ hypothesis: 'O subsistema local de spooling está operando normalmente. Nota: o estado ocioso do spooler não garante que o hardware remoto esteja ligado ou conectado neste instante.',
+ verification: 'Envie uma página de teste para validar o pipeline completo de renderização e envio.',
+ suggestedAction: 'Nenhuma ação corretiva necessária. Fila pronta para impressão.',
+ suggestedCommand: 'lp -d {queue} /usr/share/cups/data/testprint',
+ commandDesc: 'Envia a página de teste oficial do CUPS para a fila.',
+ permission: 'Permitido a qualquer usuário do sistema'
+ },
+ en: {
+ observed: 'The queue is idle and ready for incoming jobs (State 3 - Idle).',
+ origin: 'CUPS Scheduler (cupsd)',
+ hypothesis: 'Local spooling subsystem is functioning normally. Note: an idle spooler state does not guarantee physical hardware connectivity at this exact moment.',
+ verification: 'Submit a test page to validate the complete rendering and transfer pipeline.',
+ suggestedAction: 'No corrective action needed. Queue is ready to print.',
+ suggestedCommand: 'lp -d {queue} /usr/share/cups/data/testprint',
+ commandDesc: 'Submits the standard CUPS test page to the queue.',
+ permission: 'Allowed for any system user'
+ }
+ }
+ }
+ ];
+
+ /**
+ * Inicialização quando o DOM estiver pronto
+ */
+ document.addEventListener('DOMContentLoaded', function() {
+ initModeToggle();
+ initClipboard();
+ initTableSearch();
+ initKeyboardShortcuts();
+ initDestructiveConfirmations();
+ initDiagnosticAssistants();
+ initOnboardingAndTour();
+ initCommandPalette();
+
+ // Deep linking & Atalhos de Inicialização Direta via URL/Hash
+ if (window.location.hash === '#cmd-palette' || window.location.search.includes('palette=1')) {
+ setTimeout(openCommandPalette, 120);
+ } else if (window.location.hash === '#tour' || window.location.search.includes('tour=1')) {
+ setTimeout(openTourModal, 120);
+ }
+ });
+
+ /**
+ * 1. Cópia de Comandos para a Área de Transferência
+ */
+ function initClipboard() {
+ const blocks = document.querySelectorAll('.cli-block, pre.command');
+ blocks.forEach(function(block) {
+ if (block.querySelector('.copy-btn')) return;
+
+ const btn = document.createElement('button');
+ btn.type = 'button';
+ btn.className = 'copy-btn';
+ btn.setAttribute('aria-label', 'Copiar comando');
+ btn.innerHTML = '⎘ Copiar ';
+
+ btn.addEventListener('click', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ // Extrai texto ignorando o prefixo do prompt '$ '
+ let rawText = block.innerText || block.textContent;
+ // Remove rótulo do botão caso capturado
+ rawText = rawText.replace(/Copiar|Copiado!|⎘/g, '').trim();
+ // Remove prefixo '$ ' no início de linhas
+ const cleanCmd = rawText.replace(/^\$\s*/gm, '').trim();
+
+ copyTextToClipboard(cleanCmd, function(ok) {
+ if (ok) {
+ btn.classList.add('copied');
+ btn.innerHTML = '✓ Copiado! ';
+ const isPt = document.documentElement.lang ? document.documentElement.lang.startsWith('pt') : true;
+ showToast(isPt ? 'Comando copiado para a área de transferência!' : 'Command copied to clipboard!');
+ setTimeout(function() {
+ btn.classList.remove('copied');
+ btn.innerHTML = '⎘ Copiar ';
+ }, 2000);
+ }
+ });
+ });
+
+ // Se for pre.command, posiciona relativo
+ if (window.getComputedStyle(block).position === 'static') {
+ block.style.position = 'relative';
+ }
+ block.appendChild(btn);
+ });
+ }
+
+ function showToast(message, icon) {
+ icon = icon || '✓';
+ let existing = document.querySelector('.cups-toast');
+ if (existing) existing.remove();
+
+ const toast = document.createElement('div');
+ toast.className = 'cups-toast';
+ toast.setAttribute('role', 'status');
+ toast.innerHTML = `${icon} ${message} `;
+ document.body.appendChild(toast);
+
+ setTimeout(function() {
+ toast.style.transition = 'opacity 200ms ease, transform 200ms ease';
+ toast.style.opacity = '0';
+ toast.style.transform = 'translateY(8px)';
+ setTimeout(function() {
+ if (toast.parentNode) toast.parentNode.removeChild(toast);
+ }, 200);
+ }, 2400);
+ }
+
+ function copyTextToClipboard(text, callback) {
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(text).then(function() {
+ callback(true);
+ }).catch(function() {
+ fallbackCopy(text, callback);
+ });
+ } else {
+ fallbackCopy(text, callback);
+ }
+ }
+
+ function fallbackCopy(text, callback) {
+ const ta = document.createElement('textarea');
+ ta.value = text;
+ ta.style.position = 'fixed';
+ ta.style.opacity = '0';
+ document.body.appendChild(ta);
+ ta.select();
+ try {
+ const successful = document.execCommand('copy');
+ document.body.removeChild(ta);
+ callback(successful);
+ } catch (err) {
+ document.body.removeChild(ta);
+ callback(false);
+ }
+ }
+
+ /**
+ * 2. Filtragem Instantânea de Tabelas (Printers e Jobs)
+ */
+ function initTableSearch() {
+ const searchInputs = document.querySelectorAll('input[type="search"][name="QUERY"]');
+ const tables = document.querySelectorAll('table.list');
+
+ if (!tables.length || !searchInputs.length) return;
+
+ searchInputs.forEach(function(input) {
+ // Se já houver tabela, adiciona filtragem progressiva no evento input
+ input.addEventListener('input', function() {
+ const query = input.value.trim().toLowerCase();
+ tables.forEach(function(table) {
+ filterTableRows(table, query);
+ });
+ });
+ });
+ }
+
+ function filterTableRows(table, query) {
+ const tbody = table.querySelector('tbody') || table;
+ const rows = tbody.querySelectorAll('tr:not(.no-filter)');
+ let visibleCount = 0;
+
+ rows.forEach(function(row) {
+ const text = (row.textContent || row.innerText).toLowerCase();
+ if (!query || text.includes(query)) {
+ row.style.display = '';
+ visibleCount++;
+ } else {
+ row.style.display = 'none';
+ }
+ });
+
+ // Exibe ou oculta estado vazio customizado caso 0 itens
+ let emptyRow = table.querySelector('.table-empty-row');
+ if (visibleCount === 0 && query) {
+ if (!emptyRow) {
+ emptyRow = document.createElement('tr');
+ emptyRow.className = 'table-empty-row no-filter';
+ const cols = table.querySelectorAll('th').length || 5;
+ emptyRow.innerHTML = `
+ [ ! ]
+ Nenhum registro corresponde ao filtro "${escapeHtml(query)}"
+ `;
+ tbody.appendChild(emptyRow);
+ }
+ emptyRow.style.display = '';
+ } else if (emptyRow) {
+ emptyRow.style.display = 'none';
+ }
+ }
+
+ /**
+ * 3. Atalhos de Teclado Estilo Unix
+ */
+ function initKeyboardShortcuts() {
+ document.addEventListener('keydown', function(e) {
+ // Se for atalho global Ctrl+K ou Cmd+K, intercepta mesmo dentro de inputs
+ if ((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')) {
+ e.preventDefault();
+ toggleCommandPalette();
+ return;
+ }
+
+ // Se estiver digitando em um input ou textarea, não interceptar a menos que seja Esc
+ const activeTag = document.activeElement ? document.activeElement.tagName.toLowerCase() : '';
+ const isInput = activeTag === 'input' || activeTag === 'textarea' || activeTag === 'select';
+
+ if (e.key === 'Escape') {
+ // Fecha modais e desfoque de inputs
+ if (isInput) {
+ document.activeElement.blur();
+ }
+ closeHelpModal();
+ closeSettingsModal();
+ closeCommandPalette();
+ closeTourModal();
+ return;
+ }
+
+ if (isInput) return;
+
+ if (e.key === '/') {
+ // Foca no campo de busca principal
+ const searchInput = document.querySelector('input[type="search"]');
+ if (searchInput) {
+ e.preventDefault();
+ searchInput.focus();
+ searchInput.select();
+ }
+ } else if (e.key === '?') {
+ // Abre painel de ajuda/atalhos
+ e.preventDefault();
+ toggleHelpModal();
+ }
+ });
+ }
+
+ function toggleHelpModal() {
+ let modal = document.getElementById('cups-shortcuts-modal');
+ if (modal && modal.classList.contains('active')) {
+ closeHelpModal();
+ } else {
+ openHelpModal();
+ }
+ }
+
+ function openHelpModal() {
+ let modal = document.getElementById('cups-shortcuts-modal');
+ if (!modal) {
+ modal = document.createElement('div');
+ modal.id = 'cups-shortcuts-modal';
+ modal.className = 'cups-modal-overlay';
+ modal.innerHTML = `
+
+
+
+
+ / Focar campo de pesquisa
+ ? Exibir esta tela de atalhos
+ Esc Fechar janelas e desmarcar seleção
+ Tab Navegar ordenadamente pelos elementos interativos
+
+
Comandos de Linha de Comando Úteis
+
$ lpstat -p -d
+
$ lpstat -o
+
$ cupsenable <fila>
+
$ cancel <id_trabalho>
+
+
+ `;
+ document.body.appendChild(modal);
+
+ modal.querySelector('.modal-close-btn').addEventListener('click', closeHelpModal);
+ modal.addEventListener('click', function(e) {
+ if (e.target === modal) closeHelpModal();
+ });
+ initClipboard();
+ }
+ modal.classList.add('active');
+ }
+
+ function closeHelpModal() {
+ const modal = document.getElementById('cups-shortcuts-modal');
+ if (modal) modal.classList.remove('active');
+ }
+
+ function closeSettingsModal() {
+ const modal = document.getElementById('cups-ai-settings-modal');
+ if (modal) modal.classList.remove('active');
+ }
+
+ /**
+ * 4. Confirmação Proporcional de Ações Destrutivas com Alvo Explícito
+ */
+ function initDestructiveConfirmations() {
+ document.querySelectorAll('form').forEach(function(form) {
+ const opInput = form.querySelector('input[name="OP"], select[name="OP"]');
+ const submitBtn = form.querySelector('input[type="submit"]');
+
+ form.addEventListener('submit', function(e) {
+ const opVal = opInput ? (opInput.value || '') : '';
+ const btnVal = submitBtn ? (submitBtn.value || '') : '';
+
+ const isCancelJob = opVal === 'cancel-job' || btnVal.toLowerCase().includes('cancel') || btnVal.toLowerCase().includes('cancelar');
+ const isCancelAll = opVal === 'cancel-jobs' || btnVal.toLowerCase().includes('cancel all') || btnVal.toLowerCase().includes('cancelar todos');
+ const isDeletePrinter = opVal === 'delete-printer' || opVal === 'delete-class' || btnVal.toLowerCase().includes('excluir') || btnVal.toLowerCase().includes('delete');
+
+ if (isCancelJob || isCancelAll || isDeletePrinter) {
+ const jobId = form.querySelector('input[name="job_id"]')?.value;
+ const printerName = form.querySelector('input[name="printer_name"]')?.value || document.querySelector('h1.title')?.innerText || 'alvo';
+
+ let message = '';
+ if (isCancelAll) {
+ message = `Atenção: Deseja realmente CANCELAR TODOS os trabalhos da fila "${printerName}"?\nEssa ação é irreversível.`;
+ } else if (isCancelJob && jobId) {
+ message = `Confirma o cancelamento do trabalho #${jobId} na fila "${printerName}"?`;
+ } else if (isDeletePrinter) {
+ message = `Atenção crítica: Deseja realmente EXCLUIR permanentemente a fila "${printerName}" do servidor CUPS?`;
+ }
+
+ if (message && !window.confirm(message)) {
+ e.preventDefault();
+ }
+ }
+ });
+ });
+ }
+
+ /**
+ * 5. Assistência de Diagnóstico ("Root Intelligence" Contextual)
+ */
+ function initDiagnosticAssistants() {
+ const container = document.querySelector('.diagnostic-assistant-mount');
+ if (!container) return;
+
+ // Detecta contexto da página (impressora, estado, mensagem)
+ const queueName = container.getAttribute('data-queue') || '';
+ const stateCode = container.getAttribute('data-state') || '';
+ const stateMsg = container.getAttribute('data-message') || '';
+ const deviceUri = container.getAttribute('data-uri') || '';
+ const isAccepting = container.getAttribute('data-accepting') || '1';
+ const lang = document.documentElement.lang === 'en' ? 'en' : 'pt';
+
+ // Procura diagnósticos correspondentes na base determinística
+ const matchedRule = HEURISTIC_RULES.find(function(rule) {
+ return rule.match(stateCode, stateMsg, deviceUri, { accepting: isAccepting });
+ }) || HEURISTIC_RULES[HEURISTIC_RULES.length - 1]; // Fallback para idle/genérico
+
+ const diag = matchedRule.diagnostic[lang];
+ const formattedCmd = diag.suggestedCommand.replace('{queue}', queueName || 'printer');
+
+ renderDiagnosticUI(container, {
+ queueName: queueName,
+ stateCode: stateCode,
+ stateMsg: stateMsg,
+ ruleId: matchedRule.id,
+ diagnostic: diag,
+ formattedCmd: formattedCmd,
+ lang: lang
+ });
+ }
+
+ function renderDiagnosticUI(container, data) {
+ const diag = data.diagnostic;
+ const isPt = data.lang === 'pt';
+
+ const llmConfig = getLLMConfig();
+ const isLLMActive = llmConfig.enabled;
+
+ container.innerHTML = `
+
+
+
+
+
+
+
${isPt ? 'Observado' : 'Observed'}
+
+
${escapeHtml(diag.observed)}
+ ${data.stateMsg ? `
"${escapeHtml(data.stateMsg)}"
` : ''}
+
+ ${isPt ? 'Origem' : 'Origin'}: ${escapeHtml(diag.origin)}
+ ${isPt ? 'Horário' : 'Timestamp'}: ${new Date().toLocaleTimeString()}
+
+
+
+
+
+
${isPt ? 'Hipótese' : 'Hypothesis'}
+
+
${escapeHtml(diag.hypothesis)}
+
+ ${isPt ? 'Interpretação inferida a partir dos sinais coletados' : 'Inferred interpretation based on collected signals'}
+
+
+
+
+
+
${isPt ? 'Verificação' : 'Verification'}
+
+
${escapeHtml(diag.verification)}
+
+ ${isPt ? 'Próximo passo para confirmar o diagnóstico' : 'Next step to confirm diagnosis'}
+
+
+
+
+
+
${isPt ? 'Ação Sugerida' : 'Suggested Action'}
+
+
${escapeHtml(diag.suggestedAction)}
+
$ ${escapeHtml(data.formattedCmd)}
+
+ ${escapeHtml(diag.permission)}
+
+
+
+
+
+
+
+
+
+ `;
+
+ // Conectar eventos
+ const copySummaryBtn = container.querySelector('#btn-copy-support-summary');
+ if (copySummaryBtn) {
+ copySummaryBtn.addEventListener('click', function() {
+ generateSupportSummary(data);
+ });
+ }
+
+ const configAIBtn = container.querySelector('#btn-open-ai-settings');
+ if (configAIBtn) {
+ configAIBtn.addEventListener('click', function() {
+ openAISettingsModal(data);
+ });
+ }
+
+ const runLLMBtn = container.querySelector('#btn-run-llm-query');
+ if (runLLMBtn) {
+ runLLMBtn.addEventListener('click', function() {
+ queryLocalLLM(data);
+ });
+ }
+
+ initClipboard();
+ }
+
+ /**
+ * 6. Higienização e Geração de Resumo Técnico de Suporte
+ */
+ function generateSupportSummary(data) {
+ const isPt = data.lang === 'pt';
+ const diag = data.diagnostic;
+
+ const summaryText = [
+ `==================================================`,
+ `RELATÓRIO TÉCNICO DE DIAGNÓSTICO CUPS - ROOT INTEL`,
+ `Data/Hora: ${new Date().toISOString()}`,
+ `Servidor: ${window.location.hostname || 'cups-host'}`,
+ `Fila de Impressão: ${data.queueName || 'Global'}`,
+ `Código de Estado: ${data.stateCode}`,
+ `Mensagem do Dispositivo: ${data.stateMsg ? '"' + data.stateMsg + '"' : '(nenhuma mensagem)'}`,
+ `--------------------------------------------------`,
+ `[1] OBSERVADO:`,
+ ` ${diag.observed}`,
+ ``,
+ `[2] HIPÓTESE:`,
+ ` ${diag.hypothesis}`,
+ ``,
+ `[3] VERIFICAÇÃO RECOMENDADA:`,
+ ` ${diag.verification}`,
+ ``,
+ `[4] AÇÃO SUGERIDA / COMANDO:`,
+ ` ${diag.suggestedAction}`,
+ ` Comando: $ ${data.formattedCmd}`,
+ ` Permissões: ${diag.permission}`,
+ `==================================================`
+ ].join('\n');
+
+ copyTextToClipboard(summaryText, function(ok) {
+ if (ok) {
+ showToast(isPt ?
+ 'Resumo de suporte higienizado copiado com sucesso!' :
+ 'Sanitized support summary copied to clipboard!');
+ } else {
+ alert(summaryText);
+ }
+ });
+ }
+
+ /**
+ * 7. Configurações da IA Local (Ollama / Localhost)
+ */
+ function getLLMConfig() {
+ try {
+ const stored = localStorage.getItem(STORAGE_KEY_LLM);
+ if (stored) {
+ return Object.assign({}, DEFAULT_LLM_CONFIG, JSON.parse(stored));
+ }
+ } catch (e) {}
+ return DEFAULT_LLM_CONFIG;
+ }
+
+ function saveLLMConfig(config) {
+ try {
+ localStorage.setItem(STORAGE_KEY_LLM, JSON.stringify(config));
+ } catch (e) {}
+ }
+
+ function openAISettingsModal(currentData) {
+ let modal = document.getElementById('cups-ai-settings-modal');
+ const isPt = currentData.lang === 'pt';
+ const cfg = getLLMConfig();
+
+ if (!modal) {
+ modal = document.createElement('div');
+ modal.id = 'cups-ai-settings-modal';
+ modal.className = 'cups-modal-overlay';
+ document.body.appendChild(modal);
+ }
+
+ modal.innerHTML = `
+
+
+
+
+ ${isPt ?
+ 'A filosofia do CUPS Root Intelligence prioriza o processamento 100% local, autônomo e offline . O adaptador de IA conecta-se diretamente a instâncias locais (como Ollama ou Llama.cpp) rodando no seu ambiente. Nenhum conteúdo de documento, senha ou token é enviado para servidores externos.' :
+ 'CUPS Root Intelligence strictly prioritizes 100% local, autonomous, and offline execution. The AI adapter connects directly to local runtimes (like Ollama or Llama.cpp) running on your host. No document contents, passwords, or tokens are ever sent to remote clouds.'
+ }
+
+
+
+
+
+ `;
+
+ modal.classList.add('active');
+
+ modal.querySelector('.modal-close-btn').addEventListener('click', closeSettingsModal);
+ modal.querySelector('#btn-ai-modal-cancel').addEventListener('click', closeSettingsModal);
+ modal.addEventListener('click', function(e) {
+ if (e.target === modal) closeSettingsModal();
+ });
+
+ modal.querySelector('#ai-settings-form').addEventListener('submit', function(e) {
+ e.preventDefault();
+ const updated = {
+ enabled: modal.querySelector('#ai-enabled-chk').checked,
+ endpoint: modal.querySelector('#ai-endpoint-input').value.trim() || DEFAULT_LLM_CONFIG.endpoint,
+ model: modal.querySelector('#ai-model-input').value.trim() || DEFAULT_LLM_CONFIG.model,
+ timeoutMs: 15000
+ };
+ saveLLMConfig(updated);
+ closeSettingsModal();
+ initDiagnosticAssistants();
+ showToast(isPt ? 'Configuração de IA local salva com sucesso!' : 'Local AI configuration saved successfully!');
+ });
+ }
+
+ /**
+ * Consulta à IA Local com Tratamento de Erros e Timeouts Estritos
+ */
+ function queryLocalLLM(data) {
+ const isPt = data.lang === 'pt';
+ const cfg = getLLMConfig();
+ const resultBox = document.getElementById('llm-result-box');
+ if (!resultBox) return;
+
+ resultBox.style.display = 'block';
+ resultBox.innerHTML = `
+
+ ⌛
+ ${isPt ? 'Consultando modelo local em ' + escapeHtml(cfg.endpoint) + '...' : 'Querying local model at ' + escapeHtml(cfg.endpoint) + '...'}
+
+ `;
+
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), cfg.timeoutMs);
+
+ // Prompt estritamente estruturado e higienizado
+ const sanitizedPrompt = `Você é o consultor de diagnóstico técnico Unix para o servidor CUPS.
+Analise as seguintes evidências reais da fila de impressão:
+Fila: ${data.queueName}
+Código de Estado: ${data.stateCode}
+Mensagem de Erro: ${data.stateMsg || 'Nenhuma'}
+
+Responda em texto claro e direto, estruturado em exatamente 4 tópicos:
+1. Observado
+2. Hipótese
+3. Verificação (próximo passo com comando)
+4. Ação sugerida (com comando e privilégio)`;
+
+ fetch(cfg.endpoint, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ model: cfg.model,
+ prompt: sanitizedPrompt,
+ stream: false
+ }),
+ signal: controller.signal
+ })
+ .then(res => {
+ clearTimeout(timeoutId);
+ if (!res.ok) throw new Error('HTTP ' + res.status);
+ return res.json();
+ })
+ .then(json => {
+ const responseText = json.response || json.message?.content || JSON.stringify(json);
+ resultBox.innerHTML = `
+
+
+
${escapeHtml(responseText)}
+
+ `;
+ })
+ .catch(err => {
+ clearTimeout(timeoutId);
+ const isTimeout = err.name === 'AbortError';
+ const errMsg = isTimeout ?
+ (isPt ? 'Tempo excedido ao aguardar resposta do modelo local (Timeout de 15s).' : 'Connection timed out waiting for local model (15s timeout).') :
+ (isPt ? 'Falha de conexão com o runtime local (' + err.message + '). Verifique se o Ollama está em execução.' : 'Connection failure with local runtime (' + err.message + '). Verify Ollama is running.');
+
+ resultBox.innerHTML = `
+
+
+
+ ${isPt ?
+ 'O sistema permanece 100% operacional utilizando a Base de Regras Determinísticas Local exibida acima.' :
+ 'The system remains 100% operational using the Local Deterministic Rules Engine displayed above.'
+ }
+
+
+ `;
+ });
+ }
+
+ /**
+ * 7. Gerenciador de Modo de Usuário (Iniciante vs Avançado)
+ */
+ const STORAGE_KEY_MODE = 'cups_user_mode';
+
+ function initModeToggle() {
+ let savedMode = localStorage.getItem(STORAGE_KEY_MODE);
+ if (window.location.search.includes('mode=advanced')) {
+ savedMode = 'advanced';
+ } else if (window.location.search.includes('mode=beginner')) {
+ savedMode = 'beginner';
+ } else if (!savedMode || (savedMode !== 'beginner' && savedMode !== 'advanced')) {
+ savedMode = 'beginner'; // Padrão amigável e acolhedor para iniciantes
+ }
+
+ applyUserMode(savedMode, false);
+
+ const toggleBtn = document.getElementById('mode-toggle-btn');
+ if (toggleBtn) {
+ toggleBtn.addEventListener('click', function(e) {
+ e.preventDefault();
+ const currentMode = document.documentElement.classList.contains('mode-advanced') ? 'advanced' : 'beginner';
+ const newMode = currentMode === 'beginner' ? 'advanced' : 'beginner';
+ applyUserMode(newMode, true);
+ });
+ }
+ }
+
+ function applyUserMode(mode, showFeedback) {
+ const isPt = document.documentElement.lang ? document.documentElement.lang.startsWith('pt') : true;
+ localStorage.setItem(STORAGE_KEY_MODE, mode);
+
+ if (mode === 'advanced') {
+ document.documentElement.classList.remove('mode-beginner');
+ document.documentElement.classList.add('mode-advanced');
+ } else {
+ document.documentElement.classList.remove('mode-advanced');
+ document.documentElement.classList.add('mode-beginner');
+ }
+
+ const toggleBtn = document.getElementById('mode-toggle-btn');
+ if (toggleBtn) {
+ const labelSpan = toggleBtn.querySelector('.mode-label-text');
+ if (labelSpan) {
+ if (mode === 'advanced') {
+ labelSpan.textContent = isPt ? 'Modo: Avançado' : 'Mode: Advanced';
+ toggleBtn.title = isPt ? 'Clique para alternar para o Modo Iniciante' : 'Click to switch to Beginner Mode';
+ } else {
+ labelSpan.textContent = isPt ? 'Modo: Iniciante' : 'Mode: Beginner';
+ toggleBtn.title = isPt ? 'Clique para alternar para o Modo Avançado' : 'Click to switch to Advanced Mode';
+ }
+ }
+ }
+
+ if (showFeedback) {
+ if (mode === 'advanced') {
+ showToast(isPt ? 'Modo Avançado ativado: Métricas brutas e terminal habilitados.' : 'Advanced Mode activated: Raw telemetry and terminal tools enabled.', '⚡');
+ } else {
+ showToast(isPt ? 'Modo Iniciante ativado: Guias práticos e checklist visíveis.' : 'Beginner Mode activated: Guides and onboarding checklist visible.', '🌱');
+ }
+ }
+ }
+
+ /**
+ * 8. Checklist de Integração Onboarding & Tour Guiado Interativo
+ */
+ const STORAGE_KEY_ONBOARDING = 'cups_onboarding_tasks';
+
+ function initOnboardingAndTour() {
+ const isPt = document.documentElement.lang ? document.documentElement.lang.startsWith('pt') : true;
+ const banner = document.getElementById('onboarding-banner');
+ if (!banner) return;
+
+ let savedTasks = {};
+ try {
+ savedTasks = JSON.parse(localStorage.getItem(STORAGE_KEY_ONBOARDING) || '{}');
+ } catch(e) {
+ savedTasks = {};
+ }
+
+ const checkItems = banner.querySelectorAll('.check-item');
+ checkItems.forEach(function(item) {
+ const taskId = item.getAttribute('data-task');
+ const chk = item.querySelector('input[type="checkbox"]');
+ const box = item.querySelector('.check-box');
+
+ if (taskId && savedTasks[taskId] !== undefined) {
+ chk.checked = !!savedTasks[taskId];
+ }
+
+ function updateItemUI() {
+ if (chk.checked) {
+ item.classList.add('done');
+ box.textContent = '✓';
+ } else {
+ item.classList.remove('done');
+ box.textContent = '';
+ }
+ }
+
+ updateItemUI();
+
+ item.addEventListener('click', function(e) {
+ if (e.target.tagName && e.target.tagName.toLowerCase() === 'a') return;
+ e.preventDefault();
+ chk.checked = !chk.checked;
+ savedTasks[taskId] = chk.checked;
+ localStorage.setItem(STORAGE_KEY_ONBOARDING, JSON.stringify(savedTasks));
+ updateItemUI();
+ updateProgress();
+ if (chk.checked) {
+ showToast(isPt ? 'Etapa marcada como concluída!' : 'Step marked as completed!');
+ }
+ });
+ });
+
+ function updateProgress() {
+ const total = checkItems.length || 3;
+ let completed = 0;
+ checkItems.forEach(function(item) {
+ const chk = item.querySelector('input[type="checkbox"]');
+ if (chk && chk.checked) completed++;
+ });
+ const pct = Math.round((completed / total) * 100);
+ const fill = document.getElementById('onboarding-progress-fill');
+ const text = document.getElementById('onboarding-progress-text');
+ if (fill) fill.style.width = pct + '%';
+ if (text) {
+ text.textContent = isPt ? `${completed} de ${total} concluído (${pct}%)` : `${completed} of ${total} completed (${pct}%)`;
+ }
+ }
+
+ updateProgress();
+
+ const tourBtn = document.getElementById('btn-start-tour');
+ if (tourBtn) {
+ tourBtn.addEventListener('click', function(e) {
+ e.preventDefault();
+ openTourModal();
+ });
+ }
+ }
+
+ /**
+ * Modal de Tour Guiado (3 Passos)
+ */
+ function openTourModal() {
+ const isPt = document.documentElement.lang ? document.documentElement.lang.startsWith('pt') : true;
+ closeCommandPalette();
+ closeHelpModal();
+ closeSettingsModal();
+
+ let tourModal = document.getElementById('cups-tour-modal');
+ if (tourModal) tourModal.remove();
+
+ const steps = [
+ {
+ badge: isPt ? 'Passo 1 de 3: Arquitetura' : 'Step 1 of 3: Architecture',
+ title: isPt ? 'O Coração do Linux Printing' : 'The Core of Linux Printing',
+ desc: isPt ?
+ 'O CUPS orquestra o spooler local, filas de impressão e o pipeline de rasterização. Com o padrão IPP Everywhere™ , você conecta impressoras de rede sem depender de drivers proprietários ou bloatware.' :
+ 'CUPS orchestrates the local spooler, queues, and rasterization pipeline. With IPP Everywhere™ , network printers work driverless out-of-the-box.',
+ features: [
+ isPt ? '✓ Impressão direta baseada em padrões abertos' : '✓ Direct printing based on open standards',
+ isPt ? '✓ Segurança com autenticação local e sid CSRF tokens' : '✓ Local security with sid CSRF tokens',
+ isPt ? '✓ 100% autônomo e sem dependências de nuvem externa' : '✓ 100% autonomous with zero external cloud dependencies'
+ ]
+ },
+ {
+ badge: isPt ? 'Passo 2 de 3: Diagnóstico' : 'Step 2 of 3: Diagnostics',
+ title: isPt ? 'Root Intelligence & Resolução Rápida' : 'Root Intelligence & Fast Remediation',
+ desc: isPt ?
+ 'Fitas térmicas, papel e falhas de rede são diagnosticadas instantaneamente em 4 camadas (Observado, Hipótese, Verificação e Ação) com comandos de terminal prontos para copiar.' :
+ 'Media out, ribbon faults, and socket timeouts are diagnosed instantly in 4 clear layers with copy-ready terminal commands and permission indicators.',
+ features: [
+ isPt ? '✓ Base de regras determinísticas oficial e local' : '✓ Deterministic local knowledge base',
+ isPt ? '✓ Conexão opcional com IA local (Ollama offline)' : '✓ Optional connection to local AI (Ollama offline)',
+ isPt ? '✓ Cópia de relatórios higienizados para equipes de suporte' : '✓ Sanitized support summary copy button'
+ ]
+ },
+ {
+ badge: isPt ? 'Passo 3 de 3: Produtividade' : 'Step 3 of 3: Productivity',
+ title: isPt ? 'Paleta de Comandos (Ctrl+K) & Modos' : 'Command Palette (Ctrl+K) & Modes',
+ desc: isPt ?
+ 'Para usuários iniciantes ou administradores veteranos: pressione Ctrl+K em qualquer tela para buscar filas, rodar ações ou copiar comandos úteis como lpstat -p -d e cupsenable.' :
+ 'Press Ctrl+K from any page to instantly search queues, toggle between Beginner and Advanced modes, or copy vital CLI diagnostics.',
+ features: [
+ isPt ? '✓ Atalho universal Ctrl+K ou ⌘K em qualquer tela' : '✓ Universal shortcut Ctrl+K or ⌘K on any page',
+ isPt ? '✓ Alternância contínua entre Modo Iniciante e Avançado' : '✓ Seamless Beginner and Advanced mode toggle',
+ isPt ? '✓ Navegação 100% via teclado com total acessibilidade' : '✓ Full keyboard navigation & accessibility'
+ ]
+ }
+ ];
+
+ let currentStep = 0;
+
+ tourModal = document.createElement('div');
+ tourModal.id = 'cups-tour-modal';
+ tourModal.className = 'tour-modal-backdrop';
+ tourModal.setAttribute('role', 'dialog');
+ tourModal.setAttribute('aria-modal', 'true');
+ tourModal.setAttribute('aria-labelledby', 'tour-step-heading');
+
+ function renderStep() {
+ const s = steps[currentStep];
+ tourModal.innerHTML = `
+
+
+
+
${s.badge}
+
${s.title}
+
${s.desc}
+
+ ${s.features.map(f => `${f} `).join('')}
+
+
+
+
+ `;
+
+ const closeBtn = tourModal.querySelector('#tour-close-btn');
+ if (closeBtn) closeBtn.addEventListener('click', closeTourModal);
+
+ const prevBtn = tourModal.querySelector('#tour-btn-prev');
+ if (prevBtn) prevBtn.addEventListener('click', function() {
+ if (currentStep > 0) {
+ currentStep--;
+ renderStep();
+ }
+ });
+
+ const nextBtn = tourModal.querySelector('#tour-btn-next');
+ if (nextBtn) nextBtn.addEventListener('click', function() {
+ if (currentStep < steps.length - 1) {
+ currentStep++;
+ renderStep();
+ }
+ });
+
+ const finishBtn = tourModal.querySelector('#tour-btn-finish');
+ if (finishBtn) finishBtn.addEventListener('click', function() {
+ closeTourModal();
+ showToast(isPt ? 'Tour concluído! Bom proveito no CUPS.' : 'Tour complete! Enjoy CUPS.');
+ });
+ }
+
+ renderStep();
+ document.body.appendChild(tourModal);
+
+ tourModal.addEventListener('click', function(e) {
+ if (e.target === tourModal) closeTourModal();
+ });
+ }
+
+ function closeTourModal() {
+ const tourModal = document.getElementById('cups-tour-modal');
+ if (tourModal) tourModal.remove();
+ }
+
+ /**
+ * 9. Paleta de Comandos Raycast/Spotlight (Ctrl+K / ⌘K)
+ */
+ function initCommandPalette() {
+ const paletteBtn = document.getElementById('btn-cmd-palette');
+ if (paletteBtn) {
+ paletteBtn.addEventListener('click', function(e) {
+ e.preventDefault();
+ openCommandPalette();
+ });
+ }
+ }
+
+ function toggleCommandPalette() {
+ const existing = document.getElementById('cups-cmd-palette');
+ if (existing) {
+ closeCommandPalette();
+ } else {
+ openCommandPalette();
+ }
+ }
+
+ function closeCommandPalette() {
+ const palette = document.getElementById('cups-cmd-palette');
+ if (palette) palette.remove();
+ }
+
+ function openCommandPalette() {
+ const isPt = document.documentElement.lang ? document.documentElement.lang.startsWith('pt') : true;
+ closeTourModal();
+ closeHelpModal();
+ closeSettingsModal();
+
+ let palette = document.getElementById('cups-cmd-palette');
+ if (palette) palette.remove();
+
+ const items = [
+ // Ações Principais
+ {
+ group: isPt ? 'Ações Rápidas' : 'Quick Actions',
+ icon: '✦',
+ title: isPt ? 'Adicionar Nova Impressora' : 'Add New Printer',
+ sub: isPt ? 'Configurar uma nova fila de impressão local ou de rede' : 'Configure a local or network print queue',
+ badge: '/admin',
+ action: function() { window.location.href = '/admin/?OP=add-printer'; }
+ },
+ {
+ group: isPt ? 'Ações Rápidas' : 'Quick Actions',
+ icon: '🔍',
+ title: isPt ? 'Encontrar Novas Impressoras' : 'Find New Printers',
+ sub: isPt ? 'Descobrir dispositivos IPP e Bonjour/mDNS na rede' : 'Discover local IPP and mDNS broadcast printers',
+ badge: '/admin',
+ action: function() { window.location.href = '/admin/?OP=find-new-printers'; }
+ },
+ {
+ group: isPt ? 'Ações Rápidas' : 'Quick Actions',
+ icon: '🖨️',
+ title: isPt ? 'Gerenciar Todas as Impressoras' : 'Manage All Printers',
+ sub: isPt ? 'Exibir lista completa de filas e seus status' : 'View all print queues and current hardware states',
+ badge: '/printers/',
+ action: function() { window.location.href = '/printers/'; }
+ },
+ {
+ group: isPt ? 'Ações Rápidas' : 'Quick Actions',
+ icon: '📄',
+ title: isPt ? 'Gerenciar Trabalhos de Impressão' : 'Manage Print Jobs',
+ sub: isPt ? 'Visualizar fila de impressão ativa, pausar ou cancelar' : 'View spool queue, pause or cancel jobs',
+ badge: '/jobs/',
+ action: function() { window.location.href = '/jobs/'; }
+ },
+ {
+ group: isPt ? 'Ações Rápidas' : 'Quick Actions',
+ icon: '⚙️',
+ title: isPt ? 'Configurações de Administração' : 'Administration Settings',
+ sub: isPt ? 'Compartilhamento, logging de debug e permissões' : 'Server sharing, debug logging, and permissions',
+ badge: '/admin',
+ action: function() { window.location.href = '/admin/'; }
+ },
+ {
+ group: isPt ? 'Ações Rápidas' : 'Quick Actions',
+ icon: '🚀',
+ title: isPt ? 'Iniciar Tour Rápido Interativo' : 'Start Interactive Quick Tour',
+ sub: isPt ? 'Conheça os principais recursos do CUPS Root Intelligence' : 'Explore the key features of CUPS Root Intelligence',
+ badge: 'Tour',
+ action: function() { closeCommandPalette(); openTourModal(); }
+ },
+ {
+ group: isPt ? 'Modo de Usuário' : 'User Mode',
+ icon: '⚡',
+ title: isPt ? 'Alternar Modo (Iniciante ⇄ Avançado)' : 'Toggle Mode (Beginner ⇄ Advanced)',
+ sub: isPt ? 'Alterna exibição entre guias práticos e métricas brutas IPP' : 'Switch between friendly onboarding and raw IPP telemetry',
+ badge: 'Mode',
+ action: function() {
+ closeCommandPalette();
+ const currentMode = document.documentElement.classList.contains('mode-advanced') ? 'advanced' : 'beginner';
+ applyUserMode(currentMode === 'beginner' ? 'advanced' : 'beginner', true);
+ }
+ },
+ // Filas Frequentes
+ {
+ group: isPt ? 'Filas de Impressão' : 'Print Queues',
+ icon: '🖨️',
+ title: 'LaserJet-Pro-M404dn',
+ sub: isPt ? 'HP LaserJet Pro M404dn • Bancada 03 (Ociosa)' : 'HP LaserJet Pro M404dn • Bench 03 (Idle)',
+ badge: 'Queue',
+ action: function() { window.location.href = '/printers/LaserJet-Pro-M404dn'; }
+ },
+ {
+ group: isPt ? 'Filas de Impressão' : 'Print Queues',
+ icon: '⚠️',
+ title: 'Zebra-GK420t-Etiquetas',
+ sub: isPt ? 'Almoxarifado • Alerta: Fim de fita / Ribbon out' : 'Warehouse • Alert: Ribbon out',
+ badge: 'Queue',
+ action: function() { window.location.href = '/printers/Zebra-GK420t-Etiquetas'; }
+ },
+ {
+ group: isPt ? 'Filas de Impressão' : 'Print Queues',
+ icon: '🖨️',
+ title: 'WorkCentre-7835-Cor',
+ sub: isPt ? 'Xerox Multifuncional • Processando' : 'Xerox Multifunctional • Processing',
+ badge: 'Queue',
+ action: function() { window.location.href = '/printers/WorkCentre-7835-Cor'; }
+ },
+ // Comandos de Terminal
+ {
+ group: isPt ? 'Comandos de Terminal (Copiar)' : 'Terminal Commands (Copy)',
+ icon: '$',
+ title: 'lpstat -p -d',
+ sub: isPt ? 'Listar status de todas as filas e verificar qual é a padrão' : 'List status of all print queues and default destination',
+ badge: 'CLI',
+ action: function() {
+ closeCommandPalette();
+ copyTextToClipboard('lpstat -p -d', function() {
+ showToast(isPt ? 'Comando "$ lpstat -p -d" copiado!' : 'Command "$ lpstat -p -d" copied!');
+ });
+ }
+ },
+ {
+ group: isPt ? 'Comandos de Terminal (Copiar)' : 'Terminal Commands (Copy)',
+ icon: '$',
+ title: 'lpstat -o',
+ sub: isPt ? 'Consultar trabalhos ativos no spooler local do CUPS' : 'Query active print jobs in local CUPS spooler',
+ badge: 'CLI',
+ action: function() {
+ closeCommandPalette();
+ copyTextToClipboard('lpstat -o', function() {
+ showToast(isPt ? 'Comando "$ lpstat -o" copiado!' : 'Command "$ lpstat -o" copied!');
+ });
+ }
+ },
+ {
+ group: isPt ? 'Comandos de Terminal (Copiar)' : 'Terminal Commands (Copy)',
+ icon: '$',
+ title: 'cupsenable ',
+ sub: isPt ? 'Reativar fila de impressão pausada ou travada' : 'Resume a stopped or paused print queue',
+ badge: 'CLI',
+ action: function() {
+ closeCommandPalette();
+ copyTextToClipboard('cupsenable LaserJet-Pro-M404dn', function() {
+ showToast(isPt ? 'Comando "$ cupsenable" copiado!' : 'Command "$ cupsenable" copied!');
+ });
+ }
+ },
+ {
+ group: isPt ? 'Comandos de Terminal (Copiar)' : 'Terminal Commands (Copy)',
+ icon: '$',
+ title: 'cancel -a',
+ sub: isPt ? 'Cancelar todos os trabalhos pendentes em todas as filas' : 'Cancel all pending jobs across all queues',
+ badge: 'CLI',
+ action: function() {
+ closeCommandPalette();
+ copyTextToClipboard('cancel -a', function() {
+ showToast(isPt ? 'Comando "$ cancel -a" copiado!' : 'Command "$ cancel -a" copied!');
+ });
+ }
+ }
+ ];
+
+ palette = document.createElement('div');
+ palette.id = 'cups-cmd-palette';
+ palette.className = 'cmd-palette-backdrop';
+ palette.innerHTML = `
+
+ `;
+ document.body.appendChild(palette);
+
+ const input = palette.querySelector('#cmd-palette-query');
+ const resultsList = palette.querySelector('#cmd-palette-results');
+ const closeBtn = palette.querySelector('#cmd-palette-close-btn');
+
+ let selectedIdx = 0;
+ let filteredItems = items.slice();
+
+ function renderList() {
+ resultsList.innerHTML = '';
+ if (!filteredItems.length) {
+ resultsList.innerHTML = `${isPt ? 'Nenhuma ação ou fila encontrada.' : 'No matching actions or queues found.'} `;
+ return;
+ }
+
+ let currentGroup = '';
+
+ filteredItems.forEach(function(item, idx) {
+ if (item.group !== currentGroup) {
+ currentGroup = item.group;
+ const grp = document.createElement('li');
+ grp.className = 'cmd-palette-group-title';
+ grp.textContent = currentGroup;
+ resultsList.appendChild(grp);
+ }
+
+ const li = document.createElement('li');
+ li.className = 'cmd-palette-item' + (idx === selectedIdx ? ' selected' : '');
+ li.setAttribute('data-idx', idx);
+ li.innerHTML = `
+ ${escapeHtml(item.icon)}
+
+
${escapeHtml(item.title)}
+
${escapeHtml(item.sub)}
+
+ ${escapeHtml(item.badge)}
+ `;
+
+ li.addEventListener('click', function() {
+ item.action();
+ });
+
+ li.addEventListener('mouseenter', function() {
+ selectedIdx = idx;
+ updateSelectedClass();
+ });
+
+ resultsList.appendChild(li);
+ });
+
+ ensureSelectedVisible();
+ }
+
+ function updateSelectedClass() {
+ const allLis = resultsList.querySelectorAll('.cmd-palette-item');
+ allLis.forEach(function(li) {
+ const itemIdx = parseInt(li.getAttribute('data-idx'), 10);
+ if (itemIdx === selectedIdx) {
+ li.classList.add('selected');
+ } else {
+ li.classList.remove('selected');
+ }
+ });
+ }
+
+ function ensureSelectedVisible() {
+ const selectedEl = resultsList.querySelector('.cmd-palette-item.selected');
+ if (selectedEl) {
+ selectedEl.scrollIntoView({ block: 'nearest' });
+ }
+ }
+
+ input.addEventListener('input', function() {
+ const q = input.value.trim().toLowerCase();
+ if (!q) {
+ filteredItems = items.slice();
+ } else {
+ filteredItems = items.filter(function(item) {
+ return item.title.toLowerCase().includes(q) ||
+ item.sub.toLowerCase().includes(q) ||
+ item.group.toLowerCase().includes(q) ||
+ item.badge.toLowerCase().includes(q);
+ });
+ }
+ selectedIdx = 0;
+ renderList();
+ });
+
+ input.addEventListener('keydown', function(e) {
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ if (filteredItems.length > 0) {
+ selectedIdx = (selectedIdx + 1) % filteredItems.length;
+ updateSelectedClass();
+ ensureSelectedVisible();
+ }
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ if (filteredItems.length > 0) {
+ selectedIdx = (selectedIdx - 1 + filteredItems.length) % filteredItems.length;
+ updateSelectedClass();
+ ensureSelectedVisible();
+ }
+ } else if (e.key === 'Enter') {
+ e.preventDefault();
+ if (filteredItems[selectedIdx]) {
+ filteredItems[selectedIdx].action();
+ }
+ } else if (e.key === 'Escape') {
+ e.preventDefault();
+ closeCommandPalette();
+ }
+ });
+
+ closeBtn.addEventListener('click', closeCommandPalette);
+ palette.addEventListener('click', function(e) {
+ if (e.target === palette) closeCommandPalette();
+ });
+
+ renderList();
+ setTimeout(function() {
+ input.focus();
+ }, 20);
+ }
+
+ function escapeHtml(str) {
+ if (!str) return '';
+ return String(str)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+ }
+
+})();
diff --git a/templates/admin.tmpl b/templates/admin.tmpl
index 47e641acdf..8a84434116 100644
--- a/templates/admin.tmpl
+++ b/templates/admin.tmpl
@@ -1,75 +1,146 @@
-
Printers
+
+
>_ /admin/printers — Print Queues
+
Direct management of physical devices and network endpoints.
+
+
+
+
+
+
-
-
-
-
-
+
+
>_ /admin/classes — Printer Classes
+
Pool multiple printers into a single logical queue with automatic failover.
+
+
+
+
+
-
Classes
+
+
>_ /admin/jobs — Spooler & Job Management
+
Auditing, batch cancellation, hold, and release of print jobs.
+
+
+
+
+
-
-
-
-
+
+
+
>_ /admin/server — CUPS Daemon Settings
-
Jobs
+
+
+
-
-
-
-
-
-
Server
-
-
-
-
-
- {SETTINGS_ERROR?
{SETTINGS_MESSAGE}
-
{SETTINGS_ERROR} :
-
-
}
+ {SETTINGS_ERROR?
{SETTINGS_MESSAGE}
{SETTINGS_ERROR} :
+
+
}
+
diff --git a/templates/class.tmpl b/templates/class.tmpl
index de876e1248..fa93ae3582 100644
--- a/templates/class.tmpl
+++ b/templates/class.tmpl
@@ -1,41 +1,54 @@
-{printer_name}
-({printer_state=3?Idle:{printer_state=4?Processing:Paused}},
-{printer_is_accepting_jobs=0?Rejecting Jobs:Accepting Jobs},
-{server_is_sharing_printers=0?Not:{printer_is_shared=0?Not:}} Shared{default_name={printer_name}?, Server Default:})
+
-
-Description: {printer_info}
-Location: {printer_location}
-Members: {?member_uris=?None:{member_uris}}
-Defaults: job-sheets={job_sheets_default}
-media={media_default?{media_default}:unknown}
-{sides_default?sides={sides_default}:}
-
+
+
+
+ Description: {printer_info}
+ Location: {printer_location}
+ Pool Members: {?member_uris=?None:{member_uris}}
+ Defaults: job-sheets={job_sheets_default} media={media_default?{media_default}:unknown} {sides_default?sides={sides_default}:}
+ {printer_state_message?Message: "{printer_state_message}" :}
+
+
+
diff --git a/templates/classes.tmpl b/templates/classes.tmpl
index d19f433c6c..6bd286a84b 100644
--- a/templates/classes.tmpl
+++ b/templates/classes.tmpl
@@ -1,11 +1,35 @@
-{#printer_name=0?:
-
-
-Queue Name Description Location Members Status
-
-
+{#printer_name=0?
+
+
+----------------------------------------+ | [ _ ] No printer classes found | +----------------------------------------+
+
No printer classes pooled or configured.
+
+
+
+
$ lpstat -c
+
+:
+
+
+
+
+ Queue Name
+ Description
+ Location
+ Members
+ Status
+
+
+
{[printer_name]
-{printer_name} {printer_info} {printer_location} {?member_uris=?None:{member_uris}} {printer_state=3?Idle:{printer_state=4?Processing:Paused}}{printer_state_message? - "{printer_state_message}":}
+
+ {printer_name}
+ {printer_info}
+ {printer_location}
+ {?member_uris=?None:{member_uris}}
+ {printer_state=3?• Idle :{printer_state=4?• Processing :◼ Paused }}{printer_state_message?"{printer_state_message}"
:}
+
+}
+
+
+
}
-
-
}
diff --git a/templates/error.tmpl b/templates/error.tmpl
index 74a3aa39b0..06958a9025 100644
--- a/templates/error.tmpl
+++ b/templates/error.tmpl
@@ -1,5 +1,65 @@
-{?title} {?printer_name} Error
+
+
+
{?title} {?printer_name} Error
+
{?message?{message}:Error encountering operation request}:
+
-{?message?{message}:Error}:
+
+
+ ⚠
+ Mensagem do Subsistema / Subsystem Output
+
+
{error}
+
-{error}
+
+
+
+
+
+
1. Observado
+
+ Falha reportada na execução da requisição {?title}{?printer_name? ({printer_name}):} pelo daemon CUPS.
+
+
+
+
+
2. Hipótese
+
+ Permissão administrativa ausente no grupo lpadmin, backend de dispositivo inacessível na rede, ou fila inválida.
+
+
+
+
+
3. Verificação
+
+ Inspecione as últimas entradas de erro do daemon com o comando:
+
journalctl -u cups -n 25 --no-pager
+
+
+
+
+
4. Ação Sugerida
+
+ Verifique as permissões do usuário no arquivo /etc/cups/cups-files.conf ou teste o endereço do dispositivo via ping ou ippfind.
+
+
+
+
+
+
diff --git a/templates/header.tmpl b/templates/header.tmpl
index 633aacf13a..b2d12ac02d 100644
--- a/templates/header.tmpl
+++ b/templates/header.tmpl
@@ -1,5 +1,5 @@
-
+
@@ -7,8 +7,9 @@
{refresh_page? :}
-
+
+
+
{title} - {ENV:CUPS_VERSION}
-
-
+
+
diff --git a/templates/home.tmpl b/templates/home.tmpl
index 2a5c174336..f378d0bf9f 100644
--- a/templates/home.tmpl
+++ b/templates/home.tmpl
@@ -1,34 +1,123 @@
-
{ENV:CUPS_VERSION}
-
The standards-based, open source printing system developed by OpenPrinting for Linux® and other Unix®-like operating systems. CUPS uses IPP Everywhere™ to support printing to local and network printers.
+
+
+
The standards-based, open source printing system developed and maintained by OpenPrinting for Linux® and other Unix®-like operating systems. CUPS uses IPP Everywhere™ to support direct printing to local and network printers.
+
+
+
+
+
+
+
+
+
+
+
+ IPP 2.0 / Everywhere Engine Active
+
+
+ Scheduler: cupsd (pid auto)
+ Spool Dir: /var/spool/cups
+ Auth: Local / PeerCred
+ Palette: Ctrl+K
+
+
+
-
This System
-
-
-
-
-
- {#alert_title=0?:
Alerts:
-
}
+
>_ /server — System & Quick Actions
+
+
+
+
+
+
+
+
+ {#alert_title=0?:
+
}
+
+
+
+
$ lpstat -p -d
+
$ lpstat -o
+
+
diff --git a/templates/jobs-header.tmpl b/templates/jobs-header.tmpl
index a4de07b389..159a960c36 100644
--- a/templates/jobs-header.tmpl
+++ b/templates/jobs-header.tmpl
@@ -1,5 +1,11 @@
-{?which_jobs=?:
}
-{?which_jobs=completed?:
}
-{?which_jobs=all?:
}
+
diff --git a/templates/jobs.tmpl b/templates/jobs.tmpl
index e56c369c84..30ebe7a011 100644
--- a/templates/jobs.tmpl
+++ b/templates/jobs.tmpl
@@ -1,36 +1,57 @@
-{#job_id=0?:
-
-
-ID Name User Size Pages State Control
-
-
+{#job_id=0?
+
+
+----------------------------------------+ | [ _ ] No active jobs in spooler | +----------------------------------------+
+
No print jobs active or matching the selected filters.
+
$ lpstat -o
+
+:
+
+
+
+
+ ID
+ File Name
+ User
+ Size
+ Pages
+ State
+ Control Actions
+
+
+
{[job_id]
-
-{job_printer_name} -{job_id}{?phone? ({phone}):}
-{?job_name=?Unknown:{job_name}}
-{?job_originating_user_name=?Withheld:{job_originating_user_name}}
-{job_k_octets}k
-{job_impressions_completed=0?Unknown:{?job_impressions_completed}}
-{job_state=3?pending since {?date_time_at_creation=?Unknown:{date_time_at_creation}}:{job_state=4?held since {?date_time_at_creation=?Unknown:{date_time_at_creation}}:
-{job_state=5?processing since {?date_time_at_processing=?Unknown:{date_time_at_processing}}:{job_state=6?stopped:
-{job_state=7?canceled at {?date_time_at_completed=?Unknown:{date_time_at_completed}}:{job_state=8?aborted:completed at {?date_time_at_completed=?Unknown:{date_time_at_completed}}}}}}}} {job_printer_state_message?
-"{job_printer_state_message}" :}
-
-{job_preserved>0?{job_state>5?
-:}:}
-{job_state=4?{job_hold_until=auth-info-required?Held for authentication.:
-:}:}
-{job_state=3?
-:}
-{job_state<7?
-
-:}
-
-
+
+ {job_printer_name} -{job_id} {?phone? ({phone}):}
+ {?job_name=?Unknown:{job_name}}
+ {?job_originating_user_name=?Withheld:{job_originating_user_name}}
+ {job_k_octets}k
+ {job_impressions_completed=0?—:{?job_impressions_completed}}
+
+ {job_state=3?• Pending since {?date_time_at_creation=?unknown:{date_time_at_creation}}
:
+ {job_state=4?◼ Held since {?date_time_at_creation=?unknown:{date_time_at_creation}}
:
+ {job_state=5?• Processing since {?date_time_at_processing=?unknown:{date_time_at_processing}}
:
+ {job_state=6?× Stopped :
+ {job_state=7?× Canceled at {?date_time_at_completed=?unknown:{date_time_at_completed}}
:
+ {job_state=8?× Aborted :
+ ✓ Completed at {?date_time_at_completed=?unknown:{date_time_at_completed}}
}}}}}}
+ {job_printer_state_message?"{job_printer_state_message}"
:}
+
+
+
+ {job_preserved>0?{job_state>5?
+ :}:}
+ {job_state=4?
+ :}
+ {job_state=3?
+ :}
+ {job_state<7?
+
+ :}
+
+
+
}
-
-
+
+
+
}
diff --git a/templates/printer.tmpl b/templates/printer.tmpl
index afe9c2741d..350765ace8 100644
--- a/templates/printer.tmpl
+++ b/templates/printer.tmpl
@@ -1,43 +1,75 @@
-{printer_name}
-({printer_state=3?Idle:{printer_state=4?Processing:Paused}},
-{printer_is_accepting_jobs=0?Rejecting Jobs:Accepting Jobs},
-{server_is_sharing_printers=0?Not:{printer_is_shared=0?Not:}} Shared{default_name={printer_name}?, Server Default:})
+
-
-Description: {printer_info}
-Location: {printer_location}
-Driver: {printer_make_and_model} ({color_supported=1?color:grayscale}{sides_supported=one-sided?:, 2-sided printing})
- Connection: {device_uri}
-Defaults: job-sheets={job_sheets_default}
-media={media_default?{media_default}:unknown}
-{sides_default?sides={sides_default}:}
-
+
+
+
+ Description: {printer_info}
+ Location: {printer_location}
+ Driver: {printer_make_and_model} ({color_supported=1?color:grayscale}{sides_supported=one-sided?:, 2-sided printing})
+ Connection: {device_uri}
+ Defaults: job-sheets={job_sheets_default} media={media_default?{media_default}:unknown} {sides_default?sides={sides_default}:}
+ {printer_state_message?Message: "{printer_state_message}" :}
+
+
+
+
+
+
+
+
+
+
+
+
Observed: Queue {printer_name} in state {printer_state=3?Idle:{printer_state=4?Processing:Paused}}.
+
Verification via terminal: Run lpstat -p {printer_name} -l to inspect spooler details.
+
$ cupsenable {printer_name}
+
+
+
+
+
diff --git a/templates/printers.tmpl b/templates/printers.tmpl
index a80763e0c2..fa95bd233a 100644
--- a/templates/printers.tmpl
+++ b/templates/printers.tmpl
@@ -1,11 +1,36 @@
-{#printer_name=0?:
-
-
-Queue Name Description Location Make and Model Status
-
-
+{#printer_name=0?
+
+
+----------------------------------------+ | [ _ ] No active print queues found | +----------------------------------------+
+
No printers configured or matching the current search criteria.
+
+
+
+
+
$ lpstat -p -d
+
+:
+
+
+
+
+ Queue Name
+ Description
+ Location
+ Make and Model
+ Status
+
+
+
{[printer_name]
-{printer_name} {printer_info} {printer_location} {printer_make_and_model} {printer_state=3?Idle:{printer_state=4?Processing:Paused}}{printer_state_message? - "{printer_state_message}":}
+
+ {printer_name}
+ {printer_info}
+ {printer_location}
+ {printer_make_and_model}
+ {printer_state=3?• Idle :{printer_state=4?• Processing :◼ Paused }}{printer_state_message?"{printer_state_message}"
:}
+
+}
+
+
+
}
-
-
}
diff --git a/templates/pt_BR/admin.tmpl b/templates/pt_BR/admin.tmpl
index d6cf283fe3..964251d09a 100644
--- a/templates/pt_BR/admin.tmpl
+++ b/templates/pt_BR/admin.tmpl
@@ -1,77 +1,148 @@
-
-
Impressoras
+
+
>_ /admin/printers — Filas de Impressão
+
Gerenciamento direto de dispositivos físicos e conexões de rede.
+
+
+
+
+
+
-
-
-
-
-
+
+
>_ /admin/classes — Classes de Agrupamento
+
Agrupamento de múltiplas impressoras em uma única fila lógica com balanceamento.
+
+
+
+
+
-
Classes
+
+
>_ /admin/jobs — Spooler de Trabalhos
+
Auditoria, cancelamento em lote, retenção e liberação de documentos.
+
+
+
+
+
-
-
-
-
+
+
+
>_ /admin/server — Configurações do Servidor CUPS
-
Trabalhos
+
+
+
-
-
-
-
-
-
Servidor
-
-
-
-
-
- {SETTINGS_ERROR?
{SETTINGS_MESSAGE}
-
{SETTINGS_ERROR} :
-
-
}
+ {SETTINGS_ERROR?
{SETTINGS_MESSAGE}
{SETTINGS_ERROR} :
+
+
}
+
-
diff --git a/templates/pt_BR/class.tmpl b/templates/pt_BR/class.tmpl
index ec1178308d..f589e380c2 100644
--- a/templates/pt_BR/class.tmpl
+++ b/templates/pt_BR/class.tmpl
@@ -1,41 +1,54 @@
-{printer_name}
-({printer_state=3?ociosa:{printer_state=4?processando:pausada}},
-{printer_is_accepting_jobs=0?rejeitando trabalhos:aceitando trabalhos},
-{server_is_sharing_printers=0?não compartilhada:{printer_is_shared=0?não compartilhada:compartilhada}} {default_name={printer_name}?, padrão do servidor:})
+
-
-Descrição: {printer_info}
-Localização: {printer_location}
-Membros: {?member_uris=?None:{member_uris}}
-Padrões: job-sheets={job_sheets_default}
-media={media_default?{media_default}:desconhecido}
-{sides_default?sides={sides_default}:}
-
+
+
+
+ Descrição: {printer_info}
+ Localização: {printer_location}
+ Membros do Pool: {?member_uris=?Nenhum:{member_uris}}
+ Padrões: job-sheets={job_sheets_default} media={media_default?{media_default}:desconhecido} {sides_default?sides={sides_default}:}
+ {printer_state_message?Mensagem: "{printer_state_message}" :}
+
+
+
diff --git a/templates/pt_BR/classes.tmpl b/templates/pt_BR/classes.tmpl
index 7240151b54..6a505979c1 100644
--- a/templates/pt_BR/classes.tmpl
+++ b/templates/pt_BR/classes.tmpl
@@ -1,11 +1,35 @@
-{#printer_name=0?:
-
-
-Nome da fila Descrição Localização Membros Estado
-
-
+{#printer_name=0?
+
+
+----------------------------------------+ | [ _ ] Nenhuma classe de impressão | +----------------------------------------+
+
Nenhuma classe de impressoras agrupada ou configurada.
+
+
+
+
$ lpstat -c
+
+:
+
+
+
+
+ Nome da fila
+ Descrição
+ Localização
+ Membros
+ Estado
+
+
+
{[printer_name]
-{printer_name} {printer_info} {printer_location} {?member_uris=?Nenhum:{member_uris}} {printer_state=3?ociosa:{printer_state=4?processando:pausada}}{printer_state_message? - "{printer_state_message}":}
+
+ {printer_name}
+ {printer_info}
+ {printer_location}
+ {?member_uris=?Nenhum:{member_uris}}
+ {printer_state=3?• Ociosa :{printer_state=4?• Processando :◼ Pausada }}{printer_state_message?"{printer_state_message}"
:}
+
+}
+
+
+
}
-
-
}
diff --git a/templates/pt_BR/error.tmpl b/templates/pt_BR/error.tmpl
index af1c04690b..0922572560 100644
--- a/templates/pt_BR/error.tmpl
+++ b/templates/pt_BR/error.tmpl
@@ -1,5 +1,65 @@
-Erro {?title} {?printer_name}
+
+
+
Erro em {?title} {?printer_name}
+
{?message?{message}:Ocorreu um erro durante a operação solicitada}:
+
-{?message?{message}:Erro}:
+
+
+ ⚠
+ Mensagem do Subsistema
+
+
{error}
+
-{error}
+
+
+
+
+
+
1. Observado
+
+ Falha reportada na execução da requisição {?title}{?printer_name? ({printer_name}):} pelo daemon CUPS.
+
+
+
+
+
2. Hipótese
+
+ Permissão administrativa ausente no grupo lpadmin, backend de dispositivo inacessível na rede, ou fila inválida.
+
+
+
+
+
3. Verificação
+
+ Inspecione as últimas entradas de erro do daemon com o comando:
+
journalctl -u cups -n 25 --no-pager
+
+
+
+
+
4. Ação Sugerida
+
+ Verifique as permissões do usuário no arquivo /etc/cups/cups-files.conf ou teste o endereço do dispositivo via ping ou ippfind.
+
+
+
+
+
+
diff --git a/templates/pt_BR/header.tmpl b/templates/pt_BR/header.tmpl
index 9f1361ebb7..ab2ef6d050 100644
--- a/templates/pt_BR/header.tmpl
+++ b/templates/pt_BR/header.tmpl
@@ -1,5 +1,5 @@
-
+
@@ -7,8 +7,9 @@
{refresh_page? :}
-
+
+
+
{title} - {ENV:CUPS_VERSION}
-
-
+
+
diff --git a/templates/pt_BR/home.tmpl b/templates/pt_BR/home.tmpl
index 48ecdb3f00..bed057c853 100644
--- a/templates/pt_BR/home.tmpl
+++ b/templates/pt_BR/home.tmpl
@@ -1,34 +1,123 @@
-
{ENV:CUPS_VERSION}
-
CUPS é o sistema de impressão baseado em padrões e de código aberto desenvolvido pela OpenPrinting para Linux® e outros sistemas operacionais similares ao UNIX® . CUPS uses IPP Everywhere™ to support printing to local and network printers.
+
+
+
CUPS é o sistema de impressão baseado em padrões e de código aberto mantido pela OpenPrinting para Linux® e sistemas operacionais similares ao UNIX® . Utiliza o padrão IPP Everywhere™ para conexão direta a impressoras locais e de rede.
+
+
+
+
+
+
+
+
+
+
+
+ Motor IPP 2.0 / Everywhere Ativo
+
+
+ Escalonador: cupsd (pid auto)
+ Diretório de Spool: /var/spool/cups
+ Autenticação: Local / PeerCred
+ Paleta: Ctrl+K
+
+
+
-
Servidor
-
-
-
-
-
- {#alert_title=0?:
Alerts:
-
}
+
>_ /server — Servidor e Ações Rápidas
+
+
+
+
+
+
+
+
+ {#alert_title=0?:
+
+
Δ Alertas do Sistema:
+
+
}
+
+
+
+
$ lpstat -p -d
+
$ lpstat -o
+
+
diff --git a/templates/pt_BR/jobs-header.tmpl b/templates/pt_BR/jobs-header.tmpl
index 6987979ea6..de7c11e1db 100644
--- a/templates/pt_BR/jobs-header.tmpl
+++ b/templates/pt_BR/jobs-header.tmpl
@@ -1,5 +1,11 @@
-{?which_jobs=?:
}
-{?which_jobs=completed?:
}
-{?which_jobs=all?:
}
+
diff --git a/templates/pt_BR/jobs.tmpl b/templates/pt_BR/jobs.tmpl
index 6c4b6fe11a..e71f3db0f7 100644
--- a/templates/pt_BR/jobs.tmpl
+++ b/templates/pt_BR/jobs.tmpl
@@ -1,36 +1,57 @@
-{#job_id=0?:
-
-
-ID Nome Usuário Tamanho Páginas Estado Controle
-
-
+{#job_id=0?
+
+
+----------------------------------------+ | [ _ ] Nenhum trabalho no spooler | +----------------------------------------+
+
Nenhum trabalho de impressão ativo ou correspondente ao filtro selecionado.
+
$ lpstat -o
+
+:
+
+
+
+
+ ID
+ Nome do Arquivo
+ Usuário
+ Tamanho
+ Páginas
+ Estado
+ Ações de Controle
+
+
+
{[job_id]
-
-{job_printer_name} -{job_id}{?phone? ({phone}):}
-{?job_name=?Desconhecido:{job_name}}
-{?job_originating_user_name=?Retido:{job_originating_user_name}}
-{job_k_octets}k
-{job_impressions_completed=0?Desconhecido:{?job_impressions_completed}}
-{job_state=3?Pendente desde {?date_time_at_creation=?Unknown:{date_time_at_creation}}:{job_state=4?Retido desde {?date_time_at_creation=?Unknown:{date_time_at_creation}}:
-{job_state=5?Processando desde {?date_time_at_processing=?Unknown:{date_time_at_processing}}:{job_state=6?parado:
-{job_state=7?Cancelado em {?date_time_at_completed=?Unknown:{date_time_at_completed}}:{job_state=8?Abortado:Concluído em {?date_time_at_completed=?Unknown:{date_time_at_completed}}}}}}}} {job_printer_state_message?
-"{job_printer_state_message}" :}
-
-{job_preserved>0?{job_state>5?
-:}:}
-{job_state=4?
-:}
-{job_state=3?
-:}
-{job_state<7?
-
-:}
-
-
+
+ {job_printer_name} -{job_id} {?phone? ({phone}):}
+ {?job_name=?Desconhecido:{job_name}}
+ {?job_originating_user_name=?Retido:{job_originating_user_name}}
+ {job_k_octets}k
+ {job_impressions_completed=0?—:{?job_impressions_completed}}
+
+ {job_state=3?• Pendente desde {?date_time_at_creation=?desconhecido:{date_time_at_creation}}
:
+ {job_state=4?◼ Retido desde {?date_time_at_creation=?desconhecido:{date_time_at_creation}}
:
+ {job_state=5?• Processando desde {?date_time_at_processing=?desconhecido:{date_time_at_processing}}
:
+ {job_state=6?× Parado :
+ {job_state=7?× Cancelado em {?date_time_at_completed=?desconhecido:{date_time_at_completed}}
:
+ {job_state=8?× Abortado :
+ ✓ Concluído em {?date_time_at_completed=?desconhecido:{date_time_at_completed}}
}}}}}}
+ {job_printer_state_message?"{job_printer_state_message}"
:}
+
+
+
+ {job_preserved>0?{job_state>5?
+ :}:}
+ {job_state=4?
+ :}
+ {job_state=3?
+ :}
+ {job_state<7?
+
+ :}
+
+
+
}
-
-
+
+
+
}
diff --git a/templates/pt_BR/printer.tmpl b/templates/pt_BR/printer.tmpl
index ee95efd36e..dccfc0e30c 100644
--- a/templates/pt_BR/printer.tmpl
+++ b/templates/pt_BR/printer.tmpl
@@ -1,43 +1,75 @@
-{printer_name}
-({printer_state=3?ociosa:{printer_state=4?processando:pausada}},
-{printer_is_accepting_jobs=0?rejeitando trabalhos:aceitando trabalhos},
-{server_is_sharing_printers=0?não compartilhada:{printer_is_shared=0?não compartilhada:compartilhada}} {default_name={printer_name}?, padrão do servidor:})
+
-
-Descrição: {printer_info}
-Localização: {printer_location}
-Driver: {printer_make_and_model} ({color_supported=1?color:grayscale}{sides_supported?, 2-sided printing:})
- Conexão: {device_uri}
-Padrões: job-sheets={job_sheets_default}
-media={media_default?{media_default}:desconhecido}
-{sides_default?sides={sides_default}:}
-
+
+
+
+ Descrição: {printer_info}
+ Localização: {printer_location}
+ Driver: {printer_make_and_model} ({color_supported=1?color:grayscale}{sides_supported?, 2-sided printing:})
+ Conexão: {device_uri}
+ Padrões: job-sheets={job_sheets_default} media={media_default?{media_default}:desconhecido} {sides_default?sides={sides_default}:}
+ {printer_state_message?Mensagem: "{printer_state_message}" :}
+
+
+
+
+
+
+
+
+
+
+
+
Observado: Fila {printer_name} em estado {printer_state=3?Ociosa:{printer_state=4?Processando:Pausada}}.
+
Verificação via terminal: Execute lpstat -p {printer_name} -l para ver detalhes do spooler.
+
$ cupsenable {printer_name}
+
+
+
+
+
diff --git a/templates/pt_BR/printers.tmpl b/templates/pt_BR/printers.tmpl
index 74e62a20d0..69f5390e5c 100644
--- a/templates/pt_BR/printers.tmpl
+++ b/templates/pt_BR/printers.tmpl
@@ -1,11 +1,36 @@
-{#printer_name=0?:
-
-
-Nome da fila Descrição Localização Marca e modelo Estado
-
-
+{#printer_name=0?
+
+
+----------------------------------------+ | [ _ ] Nenhuma fila de impressão ativa | +----------------------------------------+
+
Nenhuma impressora configurada ou correspondente à pesquisa atual.
+
+
+
+
+
$ lpstat -p -d
+
+:
+
+
+
+
+ Nome da fila
+ Descrição
+ Localização
+ Marca e modelo
+ Estado
+
+
+
{[printer_name]
-{printer_name} {printer_info} {printer_location} {printer_make_and_model} {printer_state=3?Ociosa:{printer_state=4?Processando:Pausada}}{printer_state_message? - "{printer_state_message}":}
+
+ {printer_name}
+ {printer_info}
+ {printer_location}
+ {printer_make_and_model}
+ {printer_state=3?• Ociosa :{printer_state=4?• Processando :◼ Pausada }}{printer_state_message?"{printer_state_message}"
:}
+
+}
+
+
+
}
-
-
}
diff --git a/templates/pt_BR/search.tmpl b/templates/pt_BR/search.tmpl
index 1a16c35615..d6302cd851 100644
--- a/templates/pt_BR/search.tmpl
+++ b/templates/pt_BR/search.tmpl
@@ -1,10 +1,16 @@
-
-{WHICH_JOBS? :}
-{ORDER? :}
+
diff --git a/templates/trailer.tmpl b/templates/trailer.tmpl
index da29ea31a6..08db01c3ea 100644
--- a/templates/trailer.tmpl
+++ b/templates/trailer.tmpl
@@ -1,5 +1,8 @@
-
-
+
+
diff --git a/tools/capture_all_screens.py b/tools/capture_all_screens.py
new file mode 100644
index 0000000000..11fd5f6d0f
--- /dev/null
+++ b/tools/capture_all_screens.py
@@ -0,0 +1,48 @@
+import os
+import subprocess
+import time
+
+CHROME_PATH = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
+ARTIFACT_DIR = r"C:\Users\Targino\.gemini\antigravity-ide\brain\d2091585-ee85-46a8-aa49-603aaf3d8b75"
+
+SCENARIOS = [
+ # Desktop 1440x900
+ ("http://127.0.0.1:8631/", 1440, 900, "step3_desktop_home.png"),
+ ("http://127.0.0.1:8631/printers", 1440, 900, "step3_desktop_printers.png"),
+ ("http://127.0.0.1:8631/printers/LaserJet-Pro-M404dn", 1440, 900, "step3_desktop_printer_laserjet.png"),
+ ("http://127.0.0.1:8631/printers/Zebra-GK420t-Etiquetas", 1440, 900, "step3_desktop_printer_zebra.png"),
+ ("http://127.0.0.1:8631/jobs", 1440, 900, "step3_desktop_jobs.png"),
+ ("http://127.0.0.1:8631/admin", 1440, 900, "step3_desktop_admin.png"),
+ ("http://127.0.0.1:8631/error", 1440, 900, "step3_desktop_error.png"),
+
+ # Tablet 768x1024
+ ("http://127.0.0.1:8631/", 768, 1024, "step3_tablet_home.png"),
+ ("http://127.0.0.1:8631/printers", 768, 1024, "step3_tablet_printers.png"),
+ ("http://127.0.0.1:8631/printers/LaserJet-Pro-M404dn", 768, 1024, "step3_tablet_printer_detail.png"),
+
+ # Mobile 360x800
+ ("http://127.0.0.1:8631/", 360, 800, "step3_mobile_home.png"),
+ ("http://127.0.0.1:8631/printers", 360, 800, "step3_mobile_printers.png"),
+ ("http://127.0.0.1:8631/printers/LaserJet-Pro-M404dn", 360, 800, "step3_mobile_printer_detail.png"),
+ ("http://127.0.0.1:8631/jobs", 360, 800, "step3_mobile_jobs.png"),
+]
+
+for url, width, height, filename in SCENARIOS:
+ out_path = os.path.join(ARTIFACT_DIR, filename)
+ cmd = [
+ CHROME_PATH,
+ "--headless=new",
+ "--hide-scrollbars",
+ f"--window-size={width},{height}",
+ f"--screenshot={out_path}",
+ url
+ ]
+ print(f"Capturing {filename} ({width}x{height}) from {url}...")
+ res = subprocess.run(cmd, capture_output=True, text=True)
+ if res.returncode == 0 and os.path.exists(out_path):
+ size = os.path.getsize(out_path)
+ print(f" OK: {size} bytes")
+ else:
+ print(f" FAILED: {res.stderr}")
+
+print("Screenshot matrix capture completed!")
diff --git a/tools/capture_step4.py b/tools/capture_step4.py
new file mode 100644
index 0000000000..c38754012e
--- /dev/null
+++ b/tools/capture_step4.py
@@ -0,0 +1,28 @@
+import subprocess
+import os
+
+CHROME = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
+ARTIFACTS = r"C:\Users\Targino\.gemini\antigravity-ide\brain\d2091585-ee85-46a8-aa49-603aaf3d8b75"
+
+SCREENS = [
+ ("http://127.0.0.1:8631/", "step4_animated_home.png"),
+ ("http://127.0.0.1:8631/printers/Zebra-GK420t-Etiquetas", "step4_animated_printer_zebra.png"),
+ ("http://127.0.0.1:8631/jobs", "step4_animated_jobs.png"),
+ ("http://127.0.0.1:8631/admin", "step4_animated_admin.png"),
+]
+
+for url, filename in SCREENS:
+ out_file = os.path.join(ARTIFACTS, filename)
+ print(f"Capturing {filename} from {url}...")
+ subprocess.run([
+ CHROME,
+ "--headless=new",
+ "--hide-scrollbars",
+ "--window-size=1440,900",
+ f"--screenshot={out_file}",
+ url
+ ])
+ if os.path.exists(out_file):
+ print(f" OK: {os.path.getsize(out_file)} bytes")
+
+print("Done!")
diff --git a/tools/capture_step5.py b/tools/capture_step5.py
new file mode 100644
index 0000000000..d00eeb9e45
--- /dev/null
+++ b/tools/capture_step5.py
@@ -0,0 +1,35 @@
+import subprocess
+import os
+import time
+
+CHROME = r"C:\Program Files\Google\Chrome\Application\chrome.exe"
+ARTIFACTS = r"C:\Users\Targino\.gemini\antigravity-ide\brain\d2091585-ee85-46a8-aa49-603aaf3d8b75"
+
+TESTS = [
+ ("http://127.0.0.1:8631/?mode=beginner", "step5_desktop_home_beginner.png", (1440, 900)),
+ ("http://127.0.0.1:8631/?mode=advanced", "step5_desktop_home_advanced.png", (1440, 900)),
+ ("http://127.0.0.1:8631/?palette=1", "step5_desktop_cmd_palette.png", (1440, 900)),
+ ("http://127.0.0.1:8631/?tour=1", "step5_desktop_tour_modal.png", (1440, 900)),
+ ("http://127.0.0.1:8631/?mode=beginner", "step5_tablet_home_beginner.png", (768, 1024)),
+ ("http://127.0.0.1:8631/?mode=beginner", "step5_mobile_home_beginner.png", (360, 800)),
+ ("http://127.0.0.1:8631/?palette=1", "step5_mobile_cmd_palette.png", (360, 800)),
+]
+
+for url, filename, (width, height) in TESTS:
+ out_file = os.path.join(ARTIFACTS, filename)
+ print(f"Capturing {filename} at {width}x{height} from {url}...")
+ subprocess.run([
+ CHROME,
+ "--headless=new",
+ "--hide-scrollbars",
+ f"--window-size={width},{height}",
+ "--virtual-time-budget=2000",
+ f"--screenshot={out_file}",
+ url
+ ])
+ if os.path.exists(out_file):
+ print(f" OK: {os.path.getsize(out_file)} bytes")
+ else:
+ print(f" FAILED to generate {out_file}")
+
+print("All captures completed.")
diff --git a/tools/cups_preview_server.py b/tools/cups_preview_server.py
new file mode 100644
index 0000000000..44c9824ca7
--- /dev/null
+++ b/tools/cups_preview_server.py
@@ -0,0 +1,436 @@
+# tools/cups_preview_server.py
+"""
+Local HTTP Preview Server for CUPS Web Interface & Root Intelligence.
+Serves static assets from doc/ and dynamically renders CUPS templates with mock system state.
+Supports both pt_BR and en locales.
+"""
+import os
+import sys
+import mimetypes
+import urllib
+import urllib.parse
+from http.server import HTTPServer, BaseHTTPRequestHandler
+BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
+if BASE_DIR not in sys.path:
+ sys.path.insert(0, BASE_DIR)
+
+from tools.cups_template import CupsTemplateEngine
+PORT = 8631
+DOC_DIR = os.path.join(BASE_DIR, "doc")
+TEMPLATES_DIR = os.path.join(BASE_DIR, "templates")
+
+# Mock data simulating a realistic CUPS daemon
+MOCK_DATA = {
+ "server": {
+ "version": "CUPS 2.5b1 (Root Intelligence)",
+ "server_name": "cups-srv01.local",
+ "remote_user": "admin",
+ "remote_name": "Targino (Admin)",
+ "sid": "c9a4f2108b5e917d3324ba5c",
+ "share_printers": "CHECKED",
+ "max_clients": "100",
+ "remote_admin": "CHECKED",
+ "remote_any": "",
+ "browse_web_if": "CHECKED",
+ "user_cancel_any": "",
+ "preserve_jobs": "CHECKED",
+ "max_jobs": "500",
+ "preserve_job_history": "30d",
+ "preserve_job_files": "8h",
+ "debug_logging": "",
+ "max_log_size": "10m"
+ },
+ "printers": [
+ {
+ "printer_name": "LaserJet-Pro-M404dn",
+ "printer_info": "HP LaserJet Pro M404dn (Departamento Técnico)",
+ "printer_location": "Bancada 03 - Térreo",
+ "printer_make_and_model": "HP LaserJet Pro M404dn, driverless, cups-filters 2.0",
+ "printer_uri_supported": "/printers/LaserJet-Pro-M404dn",
+ "printer_state": "3", # 3=idle, 4=processing, 5=paused
+ "printer_state_message": "",
+ "printer_is_accepting_jobs": "1",
+ "printer_is_shared": "1",
+ "server_is_sharing_printers": "1",
+ "default_name": "LaserJet-Pro-M404dn",
+ "color_supported": "0",
+ "sides_supported": "two-sided-long-edge",
+ "device_uri": "socket://192.168.1.120:9100",
+ "job_sheets_default": "none, none",
+ "media_default": "iso_a4_210x297mm",
+ "sides_default": "two-sided-long-edge"
+ },
+ {
+ "printer_name": "Zebra-GK420t-Etiquetas",
+ "printer_info": "Impressora Térmica de Etiquetas de Inventário",
+ "printer_location": "Almoxarifado Geral",
+ "printer_make_and_model": "Zebra ZPL Label Printer, CUPS+Gutenprint",
+ "printer_uri_supported": "/printers/Zebra-GK420t-Etiquetas",
+ "printer_state": "5", # 5=paused / stopped
+ "printer_state_message": "Fim de fita / Ribbon out - sensor detectou ausência de suprimento",
+ "printer_is_accepting_jobs": "1",
+ "printer_is_shared": "0",
+ "server_is_sharing_printers": "1",
+ "default_name": "LaserJet-Pro-M404dn",
+ "color_supported": "0",
+ "sides_supported": "one-sided",
+ "device_uri": "usb://Zebra/GK420t?serial=18J143801552",
+ "job_sheets_default": "none, none",
+ "media_default": "custom_100x50mm",
+ "sides_default": "one-sided"
+ },
+ {
+ "printer_name": "WorkCentre-7835-Cor",
+ "printer_info": "Xerox WorkCentre 7835 Multifuncional Corporativa",
+ "printer_location": "Corredor Central - 2º Andar",
+ "printer_make_and_model": "Xerox WorkCentre 7835 Postscript (Color)",
+ "printer_uri_supported": "/printers/WorkCentre-7835-Cor",
+ "printer_state": "4", # 4=processing
+ "printer_state_message": "Processando página 4 de 12 (rasterizando via pdftops)",
+ "printer_is_accepting_jobs": "1",
+ "printer_is_shared": "1",
+ "server_is_sharing_printers": "1",
+ "default_name": "LaserJet-Pro-M404dn",
+ "color_supported": "1",
+ "sides_supported": "two-sided-long-edge",
+ "device_uri": "ipp://192.168.1.150/ipp/print",
+ "job_sheets_default": "none, none",
+ "media_default": "iso_a4_210x297mm",
+ "sides_default": "two-sided-long-edge"
+ }
+ ],
+ "jobs": [
+ {
+ "job_id": "1042",
+ "job_printer_name": "WorkCentre-7835-Cor",
+ "job_printer_uri": "/printers/WorkCentre-7835-Cor",
+ "job_name": "Relatorio_Trimestral_Operacoes_2026.pdf",
+ "job_originating_user_name": "m.silva",
+ "job_k_octets": "4820",
+ "job_impressions_completed": "3",
+ "job_state": "5", # 5=processing
+ "date_time_at_creation": "2026-10-20 18:45:12",
+ "date_time_at_processing": "2026-10-20 18:46:01",
+ "date_time_at_completed": "",
+ "job_printer_state_message": "Renderizando páginas 4-12",
+ "job_preserved": "1"
+ },
+ {
+ "job_id": "1041",
+ "job_printer_name": "Zebra-GK420t-Etiquetas",
+ "job_printer_uri": "/printers/Zebra-GK420t-Etiquetas",
+ "job_name": "etiquetas_patrimonio_lote_88.zpl",
+ "job_originating_user_name": "logistica",
+ "job_k_octets": "14",
+ "job_impressions_completed": "0",
+ "job_state": "6", # 6=stopped
+ "date_time_at_creation": "2026-10-20 18:30:20",
+ "date_time_at_processing": "2026-10-20 18:30:22",
+ "date_time_at_completed": "",
+ "job_printer_state_message": "Fila suspensa após erro de suprimento na impressora",
+ "job_preserved": "1"
+ },
+ {
+ "job_id": "1040",
+ "job_printer_name": "LaserJet-Pro-M404dn",
+ "job_printer_uri": "/printers/LaserJet-Pro-M404dn",
+ "job_name": "Ordem_Servico_49182.pdf",
+ "job_originating_user_name": "targino",
+ "job_k_octets": "128",
+ "job_impressions_completed": "2",
+ "job_state": "9", # 9=completed
+ "date_time_at_creation": "2026-10-20 18:12:00",
+ "date_time_at_processing": "2026-10-20 18:12:03",
+ "date_time_at_completed": "2026-10-20 18:12:18",
+ "job_printer_state_message": "",
+ "job_preserved": "1"
+ },
+ {
+ "job_id": "1039",
+ "job_printer_name": "LaserJet-Pro-M404dn",
+ "job_printer_uri": "/printers/LaserJet-Pro-M404dn",
+ "job_name": "Manual_Operacao_Linux_v2.pdf",
+ "job_originating_user_name": "f.costa",
+ "job_k_octets": "940",
+ "job_impressions_completed": "0",
+ "job_state": "4", # 4=held
+ "date_time_at_creation": "2026-10-20 17:50:11",
+ "date_time_at_processing": "",
+ "date_time_at_completed": "",
+ "job_printer_state_message": "Trabalho retido pelo operador para conferência de cota",
+ "job_preserved": "1"
+ }
+ ],
+ "classes": [
+ {
+ "printer_name": "Fila-Geral-Laser",
+ "printer_info": "Pool balanceado de impressoras Laser A4",
+ "printer_location": "Prédio Administrativo",
+ "printer_uri_supported": "/classes/Fila-Geral-Laser",
+ "member_uris": "LaserJet-Pro-M404dn",
+ "printer_state": "3",
+ "printer_state_message": "",
+ "printer_is_accepting_jobs": "1",
+ "printer_is_shared": "1",
+ "server_is_sharing_printers": "1",
+ "default_name": "LaserJet-Pro-M404dn"
+ }
+ ]
+}
+
+class CupsHandler(BaseHTTPRequestHandler):
+ def do_GET(self):
+ try:
+ self._handle_get()
+ except Exception as e:
+ import traceback
+ traceback.print_exc()
+ self.send_error(500, f"Server error: {e}")
+
+ def _handle_get(self):
+ parsed = urllib.parse.urlparse(self.path)
+ path = parsed.path
+ query = urllib.parse.parse_qs(parsed.query)
+
+ # Detect lang
+ lang = query.get("lang", ["pt_BR"])[0]
+
+ # 1. Static file handling from doc/
+ if path.startswith("/images/") or path.startswith("/help/") or path in [
+ "/cups.css", "/cups-printable.css", "/cups.js", "/apple-touch-icon.png", "/robots.txt"
+ ]:
+ rel_path = path.lstrip("/")
+ file_path = os.path.join(DOC_DIR, rel_path)
+ if os.path.isfile(file_path):
+ self.serve_file(file_path)
+ return
+ else:
+ self.send_error(404, f"File not found: {path}")
+ return
+
+ # 2. Template rendering
+ engine = CupsTemplateEngine(TEMPLATES_DIR, DOC_DIR, lang)
+
+ # Context builder
+ base_ctx = {
+ "ENV": {
+ "CUPS_VERSION": MOCK_DATA["server"]["version"],
+ "SERVER_NAME": MOCK_DATA["server"]["server_name"],
+ "REMOTE_USER": MOCK_DATA["server"]["remote_user"],
+ "REMOTE_NAME": MOCK_DATA["server"]["remote_name"],
+ "CUPS_OAUTH_SERVER": ""
+ },
+ "COOKIES": {
+ "org.cups.sid": MOCK_DATA["server"]["sid"]
+ },
+ "org.cups.sid": MOCK_DATA["server"]["sid"],
+ "QUERY": query.get("QUERY", [""])[0],
+ "alert_title": [],
+ "alert_href": []
+ }
+
+ # Routes
+ if path == "/" or path == "/index.html":
+ base_ctx["SECTION"] = "home"
+ base_ctx["title"] = "Início" if lang == "pt_BR" else "Home"
+ header = engine.render_template("header.tmpl", base_ctx, lang)
+ body = engine.render_template("home.tmpl", base_ctx, lang)
+ trailer = engine.render_template("trailer.tmpl", base_ctx, lang)
+ self.send_html(header + body + trailer)
+ return
+
+ elif path == "/printers" or path == "/printers/":
+ base_ctx["SECTION"] = "printers"
+ base_ctx["title"] = "Impressoras" if lang == "pt_BR" else "Printers"
+ base_ctx["SEARCH_DEST"] = ""
+
+ # Map printer array
+ p_list = MOCK_DATA["printers"]
+ filter_query = base_ctx["QUERY"].lower()
+ if filter_query:
+ p_list = [p for p in p_list if filter_query in p["printer_name"].lower() or filter_query in p["printer_info"].lower()]
+
+ p_ctx = dict(base_ctx)
+ p_ctx["printer_name"] = [p["printer_name"] for p in p_list]
+ p_ctx["printer_info"] = [p["printer_info"] for p in p_list]
+ p_ctx["printer_location"] = [p["printer_location"] for p in p_list]
+ p_ctx["printer_make_and_model"] = [p["printer_make_and_model"] for p in p_list]
+ p_ctx["printer_uri_supported"] = [p["printer_uri_supported"] for p in p_list]
+ p_ctx["printer_state"] = [p["printer_state"] for p in p_list]
+ p_ctx["printer_state_message"] = [p["printer_state_message"] for p in p_list]
+ p_ctx["total"] = str(len(p_list))
+
+ header = engine.render_template("header.tmpl", p_ctx, lang)
+ search = engine.render_template("search.tmpl", p_ctx, lang)
+ p_header = engine.render_template("printers-header.tmpl", p_ctx, lang)
+ printers_body = engine.render_template("printers.tmpl", p_ctx, lang)
+ trailer = engine.render_template("trailer.tmpl", p_ctx, lang)
+ self.send_html(header + search + p_header + printers_body + trailer)
+ return
+
+ elif path.startswith("/printers/"):
+ printer_name = path[len("/printers/"):]
+ base_ctx["SECTION"] = "printers"
+ base_ctx["title"] = printer_name
+
+ # Find printer
+ p_info = next((p for p in MOCK_DATA["printers"] if p["printer_name"] == printer_name), None)
+ if not p_info:
+ p_info = MOCK_DATA["printers"][0]
+
+ single_ctx = dict(base_ctx)
+ single_ctx.update(p_info)
+ single_ctx["admin_uri"] = "/admin"
+
+ # Jobs for this printer
+ p_jobs = [j for j in MOCK_DATA["jobs"] if j["job_printer_name"] == p_info["printer_name"]]
+ single_ctx["job_id"] = [j["job_id"] for j in p_jobs]
+ single_ctx["job_name"] = [j["job_name"] for j in p_jobs]
+ single_ctx["job_originating_user_name"] = [j["job_originating_user_name"] for j in p_jobs]
+ single_ctx["job_k_octets"] = [j["job_k_octets"] for j in p_jobs]
+ single_ctx["job_impressions_completed"] = [j["job_impressions_completed"] for j in p_jobs]
+ single_ctx["job_state"] = [j["job_state"] for j in p_jobs]
+ single_ctx["job_printer_name"] = [j["job_printer_name"] for j in p_jobs]
+ single_ctx["job_printer_uri"] = [j["job_printer_uri"] for j in p_jobs]
+ single_ctx["date_time_at_creation"] = [j["date_time_at_creation"] for j in p_jobs]
+ single_ctx["date_time_at_processing"] = [j["date_time_at_processing"] for j in p_jobs]
+ single_ctx["date_time_at_completed"] = [j["date_time_at_completed"] for j in p_jobs]
+ single_ctx["job_printer_state_message"] = [j["job_printer_state_message"] for j in p_jobs]
+ single_ctx["job_preserved"] = [j["job_preserved"] for j in p_jobs]
+
+ header = engine.render_template("header.tmpl", single_ctx, lang)
+ printer_body = engine.render_template("printer.tmpl", single_ctx, lang)
+ p_jobs_header = engine.render_template("printer-jobs-header.tmpl", single_ctx, lang)
+ jobs_body = engine.render_template("jobs.tmpl", single_ctx, lang)
+ trailer = engine.render_template("trailer.tmpl", single_ctx, lang)
+ self.send_html(header + printer_body + p_jobs_header + jobs_body + trailer)
+ return
+
+ elif path == "/jobs" or path == "/jobs/":
+ base_ctx["SECTION"] = "jobs"
+ base_ctx["title"] = "Trabalhos" if lang == "pt_BR" else "Jobs"
+ base_ctx["which_jobs"] = query.get("which_jobs", [""])[0]
+
+ j_list = MOCK_DATA["jobs"]
+ if base_ctx["which_jobs"] == "completed":
+ j_list = [j for j in j_list if j["job_state"] in ["7", "8", "9"]]
+ elif base_ctx["which_jobs"] == "":
+ j_list = [j for j in j_list if j["job_state"] not in ["7", "8", "9"]]
+
+ j_ctx = dict(base_ctx)
+ j_ctx["job_id"] = [j["job_id"] for j in j_list]
+ j_ctx["job_name"] = [j["job_name"] for j in j_list]
+ j_ctx["job_originating_user_name"] = [j["job_originating_user_name"] for j in j_list]
+ j_ctx["job_k_octets"] = [j["job_k_octets"] for j in j_list]
+ j_ctx["job_impressions_completed"] = [j["job_impressions_completed"] for j in j_list]
+ j_ctx["job_state"] = [j["job_state"] for j in j_list]
+ j_ctx["job_printer_name"] = [j["job_printer_name"] for j in j_list]
+ j_ctx["job_printer_uri"] = [j["job_printer_uri"] for j in j_list]
+ j_ctx["date_time_at_creation"] = [j["date_time_at_creation"] for j in j_list]
+ j_ctx["date_time_at_processing"] = [j["date_time_at_processing"] for j in j_list]
+ j_ctx["date_time_at_completed"] = [j["date_time_at_completed"] for j in j_list]
+ j_ctx["job_printer_state_message"] = [j["job_printer_state_message"] for j in j_list]
+ j_ctx["job_preserved"] = [j["job_preserved"] for j in j_list]
+
+ header = engine.render_template("header.tmpl", j_ctx, lang)
+ search = engine.render_template("search.tmpl", j_ctx, lang)
+ j_header = engine.render_template("jobs-header.tmpl", j_ctx, lang)
+ jobs_body = engine.render_template("jobs.tmpl", j_ctx, lang)
+ trailer = engine.render_template("trailer.tmpl", j_ctx, lang)
+ self.send_html(header + search + j_header + jobs_body + trailer)
+ return
+
+ elif path == "/classes" or path == "/classes/":
+ base_ctx["SECTION"] = "classes"
+ base_ctx["title"] = "Classes"
+ c_list = MOCK_DATA["classes"]
+ c_ctx = dict(base_ctx)
+ c_ctx["printer_name"] = [c["printer_name"] for c in c_list]
+ c_ctx["printer_info"] = [c["printer_info"] for c in c_list]
+ c_ctx["printer_location"] = [c["printer_location"] for c in c_list]
+ c_ctx["printer_uri_supported"] = [c["printer_uri_supported"] for c in c_list]
+ c_ctx["member_uris"] = [c["member_uris"] for c in c_list]
+ c_ctx["printer_state"] = [c["printer_state"] for c in c_list]
+ c_ctx["printer_state_message"] = [c["printer_state_message"] for c in c_list]
+ c_ctx["total"] = str(len(c_list))
+
+ header = engine.render_template("header.tmpl", c_ctx, lang)
+ search = engine.render_template("search.tmpl", c_ctx, lang)
+ c_header = engine.render_template("classes-header.tmpl", c_ctx, lang)
+ classes_body = engine.render_template("classes.tmpl", c_ctx, lang)
+ trailer = engine.render_template("trailer.tmpl", c_ctx, lang)
+ self.send_html(header + search + c_header + classes_body + trailer)
+ return
+
+ elif path == "/admin" or path == "/admin/":
+ base_ctx["SECTION"] = "admin"
+ base_ctx["title"] = "Administração" if lang == "pt_BR" else "Administration"
+ admin_ctx = dict(base_ctx)
+ admin_ctx.update(MOCK_DATA["server"])
+ admin_ctx["ADVANCEDSETTINGS"] = query.get("ADVANCEDSETTINGS", [""])[0]
+
+ header = engine.render_template("header.tmpl", admin_ctx, lang)
+ admin_body = engine.render_template("admin.tmpl", admin_ctx, lang)
+ trailer = engine.render_template("trailer.tmpl", admin_ctx, lang)
+ self.send_html(header + admin_body + trailer)
+ return
+
+ elif path == "/error":
+ base_ctx["SECTION"] = "error"
+ base_ctx["title"] = "LaserJet-Pro-M404dn"
+ base_ctx["printer_name"] = "LaserJet-Pro-M404dn"
+ base_ctx["message"] = "Falha no backend de comunicação" if lang == "pt_BR" else "Backend communication error"
+ base_ctx["error"] = "backend /usr/lib/cups/backend/socket failed: Connection timed out after 30 seconds"
+
+ header = engine.render_template("header.tmpl", base_ctx, lang)
+ err_body = engine.render_template("error.tmpl", base_ctx, lang)
+ trailer = engine.render_template("trailer.tmpl", base_ctx, lang)
+ self.send_html(header + err_body + trailer)
+ return
+
+ else:
+ self.send_error(404, f"Page not found: {path}")
+
+ def do_POST(self):
+ # Mirror form submissions gracefully
+ self.do_GET()
+
+ def send_html(self, content: str):
+ data = content.encode("utf-8")
+ self.send_response(200)
+ self.send_header("Content-Type", "text/html; charset=utf-8")
+ self.send_header("Content-Length", str(len(data)))
+ self.send_header("Cache-Control", "no-cache, no-store, must-revalidate")
+ self.end_headers()
+ self.wfile.write(data)
+
+ def serve_file(self, file_path: str):
+ mime, _ = mimetypes.guess_type(file_path)
+ if not mime:
+ if file_path.endswith(".css"): mime = "text/css"
+ elif file_path.endswith(".js"): mime = "application/javascript"
+ elif file_path.endswith(".png"): mime = "image/png"
+ elif file_path.endswith(".svg"): mime = "image/svg+xml"
+ else: mime = "application/octet-stream"
+
+ with open(file_path, "rb") as f:
+ data = f.read()
+
+ self.send_response(200)
+ self.send_header("Content-Type", mime)
+ self.send_header("Content-Length", str(len(data)))
+ self.send_header("Cache-Control", "no-cache")
+ self.end_headers()
+ self.wfile.write(data)
+
+def run_server():
+ server = HTTPServer(("127.0.0.1", PORT), CupsHandler)
+ print(f"CUPS Preview Server running at http://127.0.0.1:{PORT}/")
+ try:
+ server.serve_forever()
+ except KeyboardInterrupt:
+ print("Stopping preview server...")
+ server.server_close()
+
+if __name__ == "__main__":
+ run_server()
diff --git a/tools/cups_template.py b/tools/cups_template.py
new file mode 100644
index 0000000000..3a988ba61a
--- /dev/null
+++ b/tools/cups_template.py
@@ -0,0 +1,282 @@
+# tools/cups_template.py
+"""
+CUPS Template Engine parser in Python.
+Faithful implementation of cgi_copy from cgi-bin/template.c in CUPS.
+Used for previewing and smoke testing CUPS web interface templates.
+"""
+import re
+import html
+import urllib.parse
+from typing import Dict, List, Any, Union
+
+class CupsTemplateEngine:
+ def __init__(self, templates_dir: str, doc_dir: str, lang: str = "pt_BR"):
+ self.templates_dir = templates_dir
+ self.doc_dir = doc_dir
+ self.lang = lang
+
+ def render_template(self, tmpl_name: str, context: Dict[str, Any], lang: str = None) -> str:
+ lang_to_use = lang or self.lang
+ content = self._load_template_file(tmpl_name, lang_to_use)
+ return self._evaluate(content, context, index=0)
+
+ def _load_template_file(self, tmpl_name: str, lang: str) -> str:
+ import os
+ # 1. Try templates//
+ if lang:
+ p = os.path.join(self.templates_dir, lang, tmpl_name)
+ if os.path.isfile(p):
+ with open(p, "r", encoding="utf-8", errors="replace") as f:
+ return f.read()
+ # Try short lang e.g. pt from pt_BR
+ short_lang = lang.split("_")[0]
+ p = os.path.join(self.templates_dir, short_lang, tmpl_name)
+ if os.path.isfile(p):
+ with open(p, "r", encoding="utf-8", errors="replace") as f:
+ return f.read()
+
+ # 2. Try templates/
+ p = os.path.join(self.templates_dir, tmpl_name)
+ if os.path.isfile(p):
+ with open(p, "r", encoding="utf-8", errors="replace") as f:
+ return f.read()
+
+ raise FileNotFoundError(f"Template not found: {tmpl_name} (lang={lang})")
+
+ def _get_var(self, name: str, context: Dict[str, Any], index: int = 0) -> str:
+ # Check ENV:
+ if name.startswith("ENV:"):
+ env_name = name[4:]
+ return str(context.get("ENV", {}).get(env_name, ""))
+
+ # Check cookie $
+ if name.startswith("$"):
+ return str(context.get("COOKIES", {}).get(name[1:], ""))
+
+ # Check # count
+ if name.startswith("#"):
+ arr_name = name[1:]
+ val = context.get(arr_name, [])
+ if isinstance(val, list):
+ return str(len(val))
+ return "1" if val else "0"
+
+ # Check array indexed or current element
+ val = context.get(name)
+ if val is None:
+ # Case-insensitive fallback as CUPS variables are often case-insensitive
+ for k, v in context.items():
+ if k.lower() == name.lower():
+ val = v
+ break
+
+ if isinstance(val, list):
+ if 0 <= index < len(val):
+ return str(val[index])
+ return ""
+
+ return str(val) if val is not None else ""
+
+ def _evaluate(self, text: str, context: Dict[str, Any], index: int = 0) -> str:
+ # Recursive descent parser for CUPS curly brace syntax
+ result = []
+ i = 0
+ n = len(text)
+
+ while i < n:
+ ch = text[i]
+ if ch == '\\' and i + 1 < n and text[i+1] == '{':
+ result.append('{')
+ i += 2
+ continue
+ elif ch == '{':
+ # Parse tag
+ end_pos, tag_output = self._parse_tag(text, i, context, index)
+ result.append(tag_output)
+ i = end_pos
+ else:
+ result.append(ch)
+ i += 1
+
+ return "".join(result)
+
+ def _parse_tag(self, text: str, start: int, context: Dict[str, Any], index: int) -> tuple[int, str]:
+ # text[start] == '{'
+ # Scan until matching '}' accounting for nested braces and conditionals
+ i = start + 1
+ n = len(text)
+
+ # Check loop: {[array_name] body }
+ if i < n and text[i] == '[':
+ # find ']'
+ bracket_close = text.find(']', i)
+ if bracket_close != -1:
+ arr_name = text[i+1:bracket_close].strip()
+ # Find matching '}' for loop
+ loop_body_start = bracket_close + 1
+ loop_body_end = self._find_matching_brace(text, start)
+ loop_body = text[loop_body_start:loop_body_end-1]
+
+ # Expand loop
+ items = context.get(arr_name, [])
+ if not isinstance(items, list):
+ items = [items] if items else []
+ out_loop = []
+ for idx in range(len(items)):
+ out_loop.append(self._evaluate(loop_body, context, index=idx))
+ return loop_body_end, "".join(out_loop)
+
+ # Find matching '}' for this tag
+ tag_end = self._find_matching_brace(text, start)
+ tag_content = text[start+1:tag_end-1]
+
+ # Check if tag is a conditional with '?'
+ # Caution: question mark can be at start like {?VAR=val?...}
+ start_search = 1 if tag_content.startswith('?') else 0
+ q_pos = self._find_top_level_char(tag_content, '?', start_pos=start_search)
+ if q_pos != -1:
+ condition_expr = tag_content[:q_pos]
+ branches = tag_content[q_pos+1:]
+
+ # Find matching ':' between then and else branch
+ colon_pos = self._find_top_level_char(branches, ':')
+ if colon_pos != -1:
+ then_branch = branches[:colon_pos]
+ else_branch = branches[colon_pos+1:]
+ else:
+ then_branch = branches
+ else_branch = ""
+
+ # Evaluate condition
+ cond_val = self._eval_condition(condition_expr, context, index)
+ if cond_val:
+ return tag_end, self._evaluate(then_branch, context, index)
+ else:
+ return tag_end, self._evaluate(else_branch, context, index)
+
+ # Simple variable substitution
+ var_name = tag_content.strip()
+ if var_name.startswith("?"):
+ val = self._get_var(var_name[1:], context, index)
+ return tag_end, html.escape(val)
+ elif var_name.startswith("%"):
+ val = self._get_var(var_name[1:], context, index)
+ return tag_end, urllib.parse.quote_plus(val)
+ else:
+ val = self._get_var(var_name, context, index)
+ return tag_end, html.escape(val)
+
+ def _find_matching_brace(self, text: str, open_pos: int) -> int:
+ depth = 0
+ i = open_pos
+ n = len(text)
+ while i < n:
+ if text[i] == '{':
+ depth += 1
+ elif text[i] == '}':
+ depth -= 1
+ if depth == 0:
+ return i + 1
+ i += 1
+ return n
+
+ def _find_branch_colon(self, text: str) -> int:
+ depth = 0
+ in_tag = 0
+ first_colon = -1
+ for idx in range(len(text)):
+ ch = text[idx]
+ if ch == '{':
+ depth += 1
+ elif ch == '}':
+ depth -= 1
+ elif ch == '<':
+ in_tag += 1
+ elif ch == '>':
+ if in_tag > 0:
+ in_tag -= 1
+ elif ch == ':' and depth == 0 and in_tag == 0:
+ if first_colon == -1:
+ first_colon = idx
+ # Se for dois pontos precedido por quebra de linha (separador de bloco multilinha)
+ prev_nl = text.rfind('\n', 0, idx)
+ if prev_nl != -1 and text[prev_nl+1:idx].strip() == '':
+ return idx
+ return first_colon
+
+ def _find_top_level_char(self, text: str, target: str, start_pos: int = 0) -> int:
+ depth = 0
+ in_tag = 0
+ for idx in range(start_pos, len(text)):
+ ch = text[idx]
+ if ch == '{':
+ depth += 1
+ elif ch == '}':
+ depth -= 1
+ elif ch == '<':
+ in_tag += 1
+ elif ch == '>':
+ if in_tag > 0:
+ in_tag -= 1
+ elif ch == target and depth == 0 and in_tag == 0:
+ return idx
+ return -1
+
+ def _eval_condition(self, expr: str, context: Dict[str, Any], index: int) -> bool:
+ # expr can be:
+ # name (truthy check)
+ # ?name (truthy check)
+ # !name (falsy check)
+ # name=val
+ # ?name=val
+ # name!=val
+ # nameval
+ # name~regex
+ expr = expr.strip()
+ if not expr:
+ return False
+
+ # Operators: !=, <=, >=, =, <, >, ~
+ for op in ['!=', '<=', '>=', '=', '<', '>', '~']:
+ pos = self._find_top_level_char(expr, op[0])
+ if pos != -1 and expr[pos:pos+len(op)] == op:
+ left_var = expr[:pos].strip()
+ if left_var.startswith("?"):
+ left_var = left_var[1:]
+ right_val = expr[pos+len(op):].strip()
+
+ # evaluate right_val if it contains {var}
+ if '{' in right_val:
+ right_val = self._evaluate(right_val, context, index)
+
+ left_val = self._get_var(left_var, context, index)
+
+ if op == '=':
+ return left_val == right_val
+ elif op == '!=':
+ return left_val != right_val
+ elif op == '~':
+ try:
+ return bool(re.search(right_val, left_val))
+ except:
+ return False
+ elif op in ['<', '>', '<=', '>=']:
+ try:
+ num_left = float(left_val) if left_val else 0.0
+ num_right = float(right_val) if right_val else 0.0
+ if op == '<': return num_left < num_right
+ if op == '>': return num_left > num_right
+ if op == '<=': return num_left <= num_right
+ if op == '>=': return num_left >= num_right
+ except:
+ return False
+
+ # Single var check
+ if expr.startswith("!"):
+ v = self._get_var(expr[1:], context, index)
+ return not bool(v)
+ if expr.startswith("?"):
+ expr = expr[1:]
+ v = self._get_var(expr, context, index)
+ return bool(v)