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 = ` + + `; + 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 ? 'Assistência de Diagnóstico' : 'Diagnostic Assistant'} + [${isPt ? 'Regra Determinística Local' : 'Local Deterministic Rule'}] +
+
+ + +
+
+ +
+
+
+
${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)} +
+
+
+
+ + +
+
+ ● + + ${isLLMActive ? + `${isPt ? 'IA Local Conectada' : 'Local AI Connected'} (${escapeHtml(llmConfig.model)})` : + `${isPt ? 'IA Externa / LLM: Não configurada (Operando 100% via regras locais)' : 'External AI / LLM: Not configured (Operating 100% on local rules)'}` + } + + ${isLLMActive ? ` + + ` : ` + [${isPt ? 'Controle Local: Zero dados enviados à internet' : 'Local Control: Zero data sent to internet'}] + `} +
+ +
+
+
+ `; + + // 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 = ` + + `; + + 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 = ` +
+
+ [${isPt ? 'Resposta da IA Local Conectada: ' + escapeHtml(cfg.model) : 'Connected Local AI Response: ' + escapeHtml(cfg.model)}] +
+
${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 = ` +
+
+ ✕ ${errMsg} +
+

+ ${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 = ` +
+
+

+ >_ + ${isPt ? 'Guia Rápido do CUPS' : 'CUPS Quick Tour'} +

+ +
+
+ ${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}
    : - -
    - - - {ADVANCEDSETTINGS?

    Server Settings\:

    - -

    Advanced ▼
    - - -
    -         Max clients\: -
    -         
    -         
    -
    - {have_gssapi?
    :} -
    -
    -         Maximum jobs (0 for no limit)\: -
    -         Retain Metadata\: -
    -         Retain Documents\: -
    -
    -         Max log file size\: -

    - - :

    Server Settings:

    - -

    Advanced ▶
    - -
    -         
    -
    - {have_gssapi?
    :} -
    -

    - - } -

    - -
    } + {SETTINGS_ERROR?: + +
    + + + {ADVANCEDSETTINGS? +
    +
    + Advanced cupsd Settings • Hide Advanced ▼ +
    + + + + +
    + +
    Advertises queues via mDNS/DNS-SD (Bonjour/Avahi) on local subnet.
    +
    + +
    + +
    Limits open client sockets to prevent file descriptor exhaustion.
    +
    + +
    + +
    Permits incoming requests beyond the local subnet (0.0.0.0/0).
    +
    + +
    + +
    + +
    + +
    Allows browser-based administrative access from other IP addresses.
    +
    + + {have_gssapi?
    :} + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    Essential for root-cause diagnosis of driver and filter issues.
    +
    + +
    + +
    +
    + + : +
    +
    + Basic Server Settings • Show Advanced ▶ +
    + + + +
    + +
    + +
    + +
    + +
    + +
    + + {have_gssapi?
    :} + +
    + +
    + +
    + +
    +
    + + } +
    + +
    + +
    } +
    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:})

    +
    +
    + >_ +

    {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 Shared:{printer_is_shared=0?Not Shared: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}:}
    +
    + + + + + + + {printer_state_message?:} + +
    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}:}
    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?: - - - - - +{#printer_name=0? +
    +
    +----------------------------------------+
    |  [ _ ]  No printer classes found      |
    +----------------------------------------+
    +

    No printer classes pooled or configured.

    +
    +
    +
    +
    $ lpstat -c # List class pool members in CUPS
    +
    +: +
    +
    Queue NameDescriptionLocationMembersStatus
    + + + + + + + + + + {[printer_name] - + + + + + + + +} + +
    Queue NameDescriptionLocationMembersStatus
    {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

    +
    +
    + >_ + /system/error + [FALHA / ERROR] +
    +

    {?title} {?printer_name} Error

    +

    {?message?{message}:Error encountering operation request}:

    +
    -

    {?message?{message}:Error}:

    +
    +
    + ⚠ + Mensagem do Subsistema / Subsystem Output +
    +
    {error}
    +
    -
    {error}
    +
    +
    +
    + ⚙ + Assistência de Diagnóstico Contextual • Root Intelligence +
    + [DIAGNÓSTICO AUTOMÁTICO] +
    + +
    +
    +
    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.

    +
    +
    + >_ +

    {ENV:CUPS_VERSION}

    + [Root Intelligence • Contemporary Unix] +
    +

    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.

    +
    +
    + +
    + +
    +
    +
    + 🚀 +
    +

    Getting Started with Linux Printing

    +

    Essential checklist for new users and Linux enthusiasts • Everything runs locally

    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + 1 of 3 completed +
    +
    + + + +
    + +
    + + +
    +
    + + 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?: + } + +
    +
    Terminal Commands for Quick Diagnostics:
    +
    $ lpstat -p -d # List status of all print queues and system default printer
    +
    $ lpstat -o # Query active jobs in the local CUPS spooler
    +
    +
    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?:
    } +
    +
    + {?which_jobs=?:
    } + {?which_jobs=completed?:
    } + {?which_jobs=all?:
    } +
    -

    {?which_jobs=?Active jobs listed in processing order▼; held jobs appear first.:{which_jobs=all?Jobs in sequential order▼:Completed jobs in order of completion or cancellation▲}}

    +
    + >_ {?which_jobs=?Active jobs listed in processing order ▾ (held jobs appear first):{which_jobs=all?All jobs in sequential order ▾:Completed jobs in reverse completion order ▴}} +
    +
    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?: - - - - - +{#job_id=0? +
    +
    +----------------------------------------+
    |  [ _ ]  No active jobs in spooler    |
    +----------------------------------------+
    +

    No print jobs active or matching the selected filters.

    +
    $ lpstat -o # Inspect print job queue via terminal
    +
    +: +
    +
    IDNameUserSizePagesStateControl
    + + + + + + + + + + + + {[job_id] - - - - - - - - - + + + + + + + + + } - -
    IDFile NameUserSizePagesStateControl Actions
    {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:})

    +
    +
    + >_ +

    {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 Shared:{printer_is_shared=0?Not Shared: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}:}
    +
    + + + + + + + + {printer_state_message?:} + +
    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}:}
    Message:
    "{printer_state_message}"
    +
    + + +
    + +
    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?: - - - - - +{#printer_name=0? +
    +
    +----------------------------------------+
    |  [ _ ]  No active print queues found  |
    +----------------------------------------+
    +

    No printers configured or matching the current search criteria.

    +
    +
    +
    +
    +
    $ lpstat -p -d # Check if cupsd has detected local queues
    +
    +: +
    +
    Queue NameDescriptionLocationMake and ModelStatus
    + + + + + + + + + + {[printer_name] - + + + + + + + +} + +
    Queue NameDescriptionLocationMake and ModelStatus
    {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}
    : - -
    - - - {ADVANCEDSETTINGS?

    Configurações do servidor\:

    - -

    Avançado ▼
    - - -
    -         Máximo de clientes\: -
    -         
    -         
    -
    - {have_gssapi?
    :} -
    -
    -         Máximo de trabalhos (0 para sem limite)\: -
    -         Reter metadados\: -
    -         Reter documentos\: -
    -
    -         Tamanho máximo do arquivo de log\: -

    - - :

    Configurações do servidor:

    - -

    Avançado ▶
    - -
    -         
    -
    - {have_gssapi?
    :} -
    -

    - - } -

    - -
    } + {SETTINGS_ERROR?: + +
    + + + {ADVANCEDSETTINGS? +
    +
    + Configurações Avançadas do cupsd • Ocultar avançadas ▼ +
    + + + + +
    + +
    Anuncia as filas via mDNS/DNS-SD (Bonjour/Avahi) na rede local.
    +
    + +
    + +
    Limita descritores de socket abertos no daemon cupsd.
    +
    + +
    + +
    Permite conexões além da sub-rede local (0.0.0.0/0).
    +
    + +
    + +
    + +
    + +
    Autoriza requisições de gerenciamento via browser vindas de outros IPs.
    +
    + + {have_gssapi?
    :} + +
    + +
    Permite que usuários cancelem impressões de outros colegas sem root.
    +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    Essencial para análise de diagnóstico de falhas de filtros e drivers.
    +
    + +
    + +
    +
    + + : +
    +
    + Configurações Básicas do cupsd • Mostrar avançadas ▶ +
    + + + +
    + +
    Publica as filas na rede local via mDNS.
    +
    + +
    + +
    + +
    + +
    + + {have_gssapi?
    :} + +
    + +
    + +
    + +
    +
    + + } +
    + +
    + +
    } +
    -
    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:})

    +
    +
    + >_ +

    {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}:}
    +
    + + + + + + + {printer_state_message?:} + +
    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}:}
    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?: - - - - - +{#printer_name=0? +
    +
    +----------------------------------------+
    |  [ _ ]  Nenhuma classe de impressão    |
    +----------------------------------------+
    +

    Nenhuma classe de impressoras agrupada ou configurada.

    +
    +
    +
    +
    $ lpstat -c # Listar membros de classes no CUPS
    +
    +: +
    +
    Nome da filaDescriçãoLocalizaçãoMembrosEstado
    + + + + + + + + + + {[printer_name] - + + + + + + + +} + +
    Nome da filaDescriçãoLocalizaçãoMembrosEstado
    {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}

    +
    +
    + >_ + /sistema/erro + [FALHA / ERRO] +
    +

    Erro em {?title} {?printer_name}

    +

    {?message?{message}:Ocorreu um erro durante a operação solicitada}:

    +
    -

    {?message?{message}:Erro}:

    +
    +
    + ⚠ + Mensagem do Subsistema +
    +
    {error}
    +
    -
    {error}
    +
    +
    +
    + ⚙ + Assistência de Diagnóstico Contextual • Root Intelligence +
    + [DIAGNÓSTICO AUTOMÁTICO] +
    + +
    +
    +
    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.

    +
    +
    + >_ +

    {ENV:CUPS_VERSION}

    + [Root Intelligence • Unix Contemporâneo] +
    +

    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.

    +
    +
    + +
    + +
    +
    +
    + 🚀 +
    +

    Primeiros Passos no Linux Printing

    +

    Guia prático para iniciantes e entusiastas no mundo Linux • 100% autônomo e local

    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + 1 de 3 concluído +
    +
    + + + +
    + +
    + + +
    +
    + + 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?: + } + +
    +
    Comandos de Terminal para Diagnóstico Rápido:
    +
    $ lpstat -p -d # Exibir status de todas as filas e verificar qual é o destino padrão
    +
    $ lpstat -o # Consultar trabalhos ativos no spooler local do CUPS
    +
    +
    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?:
    } +
    +
    + {?which_jobs=?:
    } + {?which_jobs=completed?:
    } + {?which_jobs=all?:
    } +
    -

    {?which_jobs=?Active jobs listed in processing order▼; held jobs appear first.:{which_jobs=all?Jobs in sequential order▼:Completed jobs in order of completion or cancellation▲}}

    +
    + >_ {?which_jobs=?Trabalhos ativos listados em ordem de processamento ▾ (trabalhos retidos aparecem primeiro):{which_jobs=all?Todos os trabalhos em ordem sequencial ▾:Trabalhos concluídos em ordem decrescente ▴}} +
    +
    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?: - - - - - +{#job_id=0? +
    +
    +----------------------------------------+
    |  [ _ ]  Nenhum trabalho no spooler  |
    +----------------------------------------+
    +

    Nenhum trabalho de impressão ativo ou correspondente ao filtro selecionado.

    +
    $ lpstat -o # Verificar a fila de trabalhos via terminal
    +
    +: +
    +
    IDNomeUsuárioTamanhoPáginasEstadoControle
    + + + + + + + + + + + + {[job_id] - - - - - - - - - + + + + + + + + + } - -
    IDNome do ArquivoUsuárioTamanhoPáginasEstadoAções de Controle
    {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:})

    +
    +
    + >_ +

    {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}:}
    +
    + + + + + + + + {printer_state_message?:} + +
    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}:}
    Mensagem:
    "{printer_state_message}"
    +
    + + +
    + +
    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?: - - - - - +{#printer_name=0? +
    +
    +----------------------------------------+
    |  [ _ ]  Nenhuma fila de impressão ativa  |
    +----------------------------------------+
    +

    Nenhuma impressora configurada ou correspondente à pesquisa atual.

    +
    +
    +
    +
    +
    $ lpstat -p -d # Verificar se o scheduler CUPS detectou filas locais
    +
    +: +
    +
    Nome da filaDescriçãoLocalizaçãoMarca e modeloEstado
    + + + + + + + + + + {[printer_name] - + + + + + + + +} + +
    Nome da filaDescriçãoLocalizaçãoMarca e modeloEstado
    {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?:} +
    + +{WHICH_JOBS?:} +{ORDER?:} -

    Pesquisar em -{SEARCH_DEST?{SEARCH_DEST}:{SECTION=classes?classes:{SECTION=jobs?trabalhos:impressoras}}}: -

    +
    + + + + +
    - + +
    diff --git a/templates/pt_BR/trailer.tmpl b/templates/pt_BR/trailer.tmpl index c9ef8b3529..29e2ab9064 100644 --- a/templates/pt_BR/trailer.tmpl +++ b/templates/pt_BR/trailer.tmpl @@ -1,5 +1,8 @@ - - + +
    + + +
    diff --git a/templates/search.tmpl b/templates/search.tmpl index 755269ea37..9ef89da1b0 100644 --- a/templates/search.tmpl +++ b/templates/search.tmpl @@ -1,10 +1,16 @@ -
    -{WHICH_JOBS?:} -{ORDER?:} +
    + +{WHICH_JOBS?:} +{ORDER?:} -

    Search in -{SEARCH_DEST?{SEARCH_DEST}:{SECTION=classes?Classes:{SECTION=jobs?Jobs:Printers}}}: -

    +
    + + + + +
    - + +
    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)