# Painel — a tela que presta

> QUANDO LER: **antes da primeira linha de HTML** de qualquer tela, painel ou gráfico que ela vai abrir.

Uma tela boa é uma tela que ela **abre no celular, entende em três segundos e confia**.
Feio não é questão de gosto: feio é o número que ela veio ver não estar na primeira tela.

---

## PARTE 0 — DE ONDE VEM O DADO DA TELA (leia antes de tudo)

**A sua oficina NÃO fala com o banco.** Ela não tem senha do Sankhya e nunca vai ter (lá dentro
você é root, e senha em arquivo que o modelo lê é senha vazada). Quem fala com o banco é o **app**,
com o acesso **da pessoa**.

**A tomada:** registre a fonte uma vez e a tela busca sozinha, para sempre. **Duas fontes:**

```
mcp__nix__publica_dados   nome: "producao-dia"   sql: "SELECT ..."          cache_seg: 300
mcp__nix__publica_dados   nome: "tempo-bh"       url: "https://api...json"  cache_seg: 600
```

Na tela:

```js
const r = await fetch('/api/meu/dados/producao-dia');   // o app roda o SELECT com o acesso DELA
const D = await r.json();      // banco:    { colunas: [...], linhas: [[...]], quando, do_cache }
                               // internet: { json: {...}, quando, do_cache }
```

**Dado de FORA (tempo, cotação, feriado, câmbio) também é tomada** — `url:` em vez de `sql:`.
Quem busca é a casa, não a sua oficina. É a mesma tela, a mesma rota, o mesmo cache.

### O número na TELA INICIAL (acima da marca): `.nexum/faixa.json`

Quando ela disser *"quero X na minha tela inicial"*, não é painel nem robô — é a faixa:

```json
{"dados": "tempo-bh",
 "grande": "{current_condition.0.temp_C}°C",
 "linha":  "{current_condition.0.lang_pt.0.value} · mín {weather.0.mintempC}° / máx {weather.0.maxtempC}°"}
```

Os `{caminhos}` são preenchidos pelo navegador com o dado da tomada, **toda vez que a tela abre**.
No banco, o caminho é o **nome da coluna** (`{M3_HOJE}`). Faixa sem dado simplesmente não aparece.

- **É AO VIVO.** A pessoa recarrega, o número vem do banco (respeitando `cache_seg`, que é a
  validade do resultado — ponha 0 se ela quiser o número do segundo).
- **NUNCA grave um `.json` de dados na oficina** e **NUNCA monte robô/cron para atualizar número
  de painel.** Robô que regrava número **custa uma mensagem dela por atualização** (o cron acorda o
  seu cérebro), demora minutos e ainda entrega dado velho entre uma rodada e outra. *Isto já
  aconteceu: um pedido que era "só uma query" virou robô + script + arquivo.* **Era pra ser a
  tomada.**
- **O painel abre pelo MENU dela** (é lá que o crachá dela existe). O endereço público do container
  não tem sessão — uma tela aberta por fora não recebe dado, e é assim mesmo.
- Robô continua existindo para o que é robô: **avisar quando algo foge do normal** (caderno `robo.md`).

---

## PARTE 1 — AS TRÊS QUE NÃO PODEM FALHAR

1. **O NÚMERO QUE ELA PEDIU APARECE NO CELULAR, SEM ARRASTAR NADA.**
   Já aconteceu: painel de vendas em que a coluna **m³** — o número inteiro do pedido — ficava
   **fora da tela do celular**. Aparecia "viagens" e mais nada. A tabela tinha `min-width:620px`
   dentro de um `overflow-x:auto`, então "tecnicamente rolava de lado". Ninguém rola de lado.
   **A tabela larga não é solução, é o defeito.** Veja a Parte 5: no celular, colunas somem,
   não encolhem.
2. **NÃO SOME PREÇO UNITÁRIO.** `SUM(VALORVENDA)` é somar R$/m³ linha a linha: dá um número
   plausível e **completamente falso**. Já aconteceu: o painel mostrou **"R$ 60/m³"** quando o
   preço real é **R$ 476/m³**. Preço médio é `SUM(VALORTOTALSERVICO) / SUM(QUANTIDADE)`.
   **Toda média é divisão de duas somas** — nunca a média das médias. (Vale igual pra
   m³/viagem: `SUM(m³)/SUM(viagens)`, nunca `AVG(m3_por_viagem)`.)
3. **OLHE A TELA — COM `mcp__nix__print`, NOS DOIS TAMANHOS (celular e computador).**
   Você TEM olhos: `mcp__nix__print` abre a página num navegador de verdade, salva o print
   na sua pasta (abra com **Read** e VEJA) e devolve os erros de JavaScript. Não tente
   instalar navegador na oficina — ela não tem, e o print sai da mãe.
   **`curl` devolvendo 200 não prova nada.** Aconteceu aqui em 13/07: o painel de produção
   foi entregue com o gráfico "Ritmo do dia" **completamente vazio** — as barras tinham
   `height:83%` dentro de um pai **sem altura**, então valiam zero. A página respondia 200,
   o HTML estava lá, e a pessoa via um retângulo em branco. **Barra com altura em % exige
   pai com altura fixa** (`height:160px` no contêiner das barras).
   E não basta fotografar parado: **clique**, abra um nível, e olhe de novo. Foi clicando
   que se descobriu que a barra de proporção de 30px mostrava o mesmo traço pra 98% e pra
   4% — ela não estava mostrando nada.

---

## PARTE 2 — A FORMA: A PERGUNTA ESCOLHE O MODELO (não o seu gosto)

**Antes de desenhar, responda em voz alta: que pergunta esta tela responde?** A pergunta manda
na forma — e às vezes a resposta **não é gráfico**: um número só é um **número grande na tela**,
não uma rosquinha. Mais de 7 categorias → **tabela**, não gráfico.

Estes são os **sete modelos**. Quase todo pedido que ela faz cai num deles (ou em dois, empilhados
na mesma tela). Escolha o modelo primeiro, o HTML depois.

| # | A pergunta dela | Modelo | O gráfico |
|---|---|---|---|
| 1 | "Como estamos?" (15 segundos, uma vez por dia) | **Executivo** | Número herói + azulejos + linha de tendência |
| 2 | "Como está o dia?" (ela olha várias vezes) | **Operacional do dia** | Barras por período + tabela de "quem" |
| 3 | "Quem é o maior / o pior?" | **Ranking** | Barras **horizontais**, ordenadas |
| 4 | "Do que isso é feito, e mudou?" | **Composição** | Barras **empilhadas** no tempo |
| 5 | "Estamos batendo a meta?" | **Meta × realizado** | Uma barra por unidade + **traço da meta** |
| 6 | "Onde está o gargalo?" | **Mapa de calor** | Grade dia × hora, do apagado ao aceso |
| 7 | "O que eu preciso olhar AGORA?" | **Alertas & status** | Lista curta de exceção, com estado e ação |

### Como cada um é por dentro

1. **EXECUTIVO.** UM número herói (o que ela veio ver) + 3 ou 4 azulejos de contexto + um gráfico
   de tendência (linha, com o valor aparecendo ao passar o dedo/mouse). *Armadilha:* dois números
   heróis não é ênfase, é nenhum. Se tudo é destaque, nada é.
2. **OPERACIONAL DO DIA.** Em cima, o **quanto** (barras por dia/hora); embaixo, o **quem**
   (tabela ordenada, clicável). Filtros no topo. *Armadilha:* barra sem pai com altura fixa sai
   em branco — e a página responde 200 do mesmo jeito.
3. **RANKING.** Barra **horizontal** (o nome cabe do lado, sem virar a cabeça), ordenada do maior
   pro menor, **rótulo direto na barra**. Mostre o **top 7 + "outros"** — lista de 40 linhas não
   é ranking, é tabela. *Armadilha:* pintar cada barra de uma cor. Ranking é UMA série: uma cor.
4. **COMPOSIÇÃO.** Barras empilhadas ao longo do tempo, **2px de vão** entre as fatias, **legenda
   sempre**, rótulo só nas fatias grandes, e um botão **"ver tabela"** pra quem quer o número exato.
   *Armadilha:* mais de 5 fatias vira sopa — junte o resto em "outros".
5. **META × REALIZADO.** Uma linha por unidade (usina, obra, vendedor): a **barra** é o realizado,
   um **traço claro** é a meta, e o **rótulo diz quanto falta** ("−140 m³"). *Armadilha:* barra que
   passa de 100% tem de continuar dentro do trilho — não estoure o cartão.
6. **MAPA DE CALOR.** Grade dia × hora (ou usina × dia): uma cor só, do **apagado ao aceso** —
   escala **sequencial**, nunca cores diferentes por célula. Serve pra achar pico e buraco.
   *Armadilha:* célula pequena sem tooltip é enfeite. Todo quadradinho mostra o valor ao tocar.
7. **ALERTAS & STATUS.** Lista **curta** só do que fugiu do normal; cada item com o estado
   (**cor + PALAVRA**, nunca cor sozinha) e a ação do lado. *Armadilha:* listar tudo. Se está
   normal, não entra — alerta que aparece sempre vira paisagem e a pessoa para de olhar.

### E os CINCO DE TELA — quando a resposta não é gráfico (13/07)

Os sete respondem **"quanto?"**. O resto do trabalho dela é **conferir**, **decidir**,
**investigar**, **entrar** e **procurar** — e aí a forma é outra. O código dos cinco está na skill
`painel` → `referencias/modelos.md`.

| # | A pergunta dela | Modelo | O jeito |
|---|---|---|---|
| 8 | "Me deixa conferir linha por linha" (10+ colunas) | **Matriz** | Tabela larga com **1ª coluna congelada** + total no pé + trio de resumo antes |
| 9 | "O que eu aprovo?" | **Decisão** | Cartão com os fatos + Aprovar/Reprovar + barra fixa: *"2 marcados · 5 sem marcação (não mexo)"* |
| 10 | "Por que esse número está assim?" | **Analítico** | Seletor de visão + abrir nível + **filtro no cabeçalho da coluna** + Colunas. **Sem paginação.** |
| 11 | "Por onde eu começo?" (o módulo tem 3+ telas) | **Menu em grade** | Portas 2×2 com **selo do que espera decisão dela** |
| 12 | "Quanto é o bombeado 30 na Betim?" | **Busca** | Caixa de busca + pílulas de onde procurar + quadro por unidade |

**O modelo 9 é o que muda o jogo:** painel que só **mostra** o problema é metade do serviço; o que
**resolve** na tela é ferramenta. Só que — 🔴 **escrita no ERP não sai de um clique**: o botão monta
a proposta, e quem confirma é a **pessoa digitando o código de 4 dígitos no chat**.

### Cinco proibições — valem em qualquer um dos sete

- **ZERO PIZZA / ROSQUINHA.** Ninguém compara ângulo. Duas fatias? Barra. Composição no tempo?
  Empilhada. Um número só? Número grande.
- **NUNCA DOIS EIXOS Y** na mesma figura (m³ de um lado, R$ do outro). Isso desenha uma correlação
  que não existe. Duas escalas = dois gráficos.
- **NUNCA RÓTULO EM TODO PONTO.** Só no pico, no fim da linha ou no que ela veio ver.
- **ESTADO NUNCA É SÓ COR.** Cor **e** palavra ("atrasado", "ok") — tem gente que não distingue
  verde de vermelho, e todo mundo lê no sol, na tela suja, no celular ruim.
- **A COR É DA ENTIDADE, NUNCA DA POSIÇÃO.** Se a BetMix Betim é azul num gráfico, ela é azul em
  **todos** os gráficos daquela tela e das próximas. Cor tirada do índice do laço (`cores[i]`) faz
  a mesma usina trocar de cor quando ela muda o filtro — e a pessoa lê errado sem saber.
  O jeito certo é um **mapa fixo, escrito no topo do arquivo**:

```js
// a cor sai do NOME, não da ordem. Entidade nova entra aqui, não no laço.
const COR = {'BetMix Betim':'#3987e5', 'BetMix Contagem':'#199e70', ...};
const cor = e => COR[e] || '#5F6B7C';   // quem não está no mapa é cinza, não é colorido
```

### Todo gráfico tem tooltip e "ver tabela"

Gráfico em HTML **é** interativo — se ela toca e não acontece nada, o dado ficou preso no desenho.
- **Tooltip por marca** (barra, ponto, célula do mapa de calor): o valor exato, formatado em pt-BR.
- **Botão "ver tabela"** embaixo de todo gráfico: abre a mesma coisa em linhas e números. O gráfico
  é pra ENXERGAR; a tabela é pra CONFERIR — e sempre tem alguém que precisa conferir.
- **Legenda sempre que houver 2 séries ou mais.** Uma série só: sem legenda, o título já diz.

---

## PARTE 2B — AS QUATRO REGRAS DE CONTEÚDO (a maioria dos painéis ruins erra AQUI, não no gráfico)

1. **NÚMERO SOZINHO NÃO DIZ NADA — todo número importante vem com COMPARAÇÃO.**
   "28.019 m³" é enfeite. "**28.019 m³ — ▲6,4% vs junho · meta 32.000**" é informação. A referência
   é uma destas três: **período anterior**, **meta** ou **média**. Se você não tem nenhuma delas,
   busque uma antes de entregar a tela — é uma consulta a mais, e é ela que faz a pessoa saber se
   o número é bom ou ruim.
2. **DIGA DE ONDE VEIO E A QUE HORAS.** Rodapé em toda tela: *"fonte: CND_VGF_AD_VENDAS · gerado
   09:42"*. Sem isso, na primeira divergência o painel perde a confiança e ninguém abre de novo.
   E diga **o que ficou de fora** ("171 m³ sem hora registrada não entraram no gráfico").
3. **ESTADO VAZIO TEM SAÍDA.** Deu erro? *"não consegui buscar agora"* **+ botão "Tentar de novo"**.
   Nunca tela em branco, nunca `0` (zero é plausível — ela lê "não vendemos nada" e decide errado
   em cima de um defeito seu), nunca "carregando…" pra sempre.
4. **SEM PAGINAÇÃO em painel gerencial.** Soma no servidor + níveis que abrem + rolagem. Paginação
   esconde o total e obriga a pessoa a somar página com página.

### As quatro perguntas ANTES de desenhar (pergunte a ela — não chute)

Quando o pedido chegar vago ("faz um painel de vendas"), **não trabalhe dez minutos calado**.
Estas quatro respostas custam uma mensagem e evitam a tela que ninguém pediu:

1. **Que pergunta a tela responde?** (uma frase — é ela que escolhe o modelo)
2. **Quem abre, e onde?** (celular na obra? computador na sala?)
3. **De onde vem o número?** (qual tabela/view — e com qual filtro)
4. **Dá pra agir na tela** (aprovar, marcar, lançar) **ou é só olhar?**

Se der pra responder as quatro sozinho com o que você já sabe dela, **responda e siga** — não
pergunte o que já está na sua memória.

---

## PARTE 3 — A ESTÉTICA (isto é regra, não gosto)

### A paleta — uma cor de dado, o resto é cinza
A cor é da marca **niX**, não sua. Fundo preto, azul de destaque, tinta branca.

```
--fundo:#0B0F14   fundo da página
--surf:#151A21    cartão
--surf2:#0F141B   campo/afundado
--linha:#242B35   borda do cartão
--linha2:#1B212A  divisória entre linhas (mais fraca que a borda)
--tinta:#EDF1F6   texto principal
--tinta2:#93A0B4  rótulo, texto de apoio
--tinta3:#5F6B7C  nota, rodapé, eixo
--azul:#2F7DF6    A COR DO DADO
```

**Uma série de dado = UMA cor.** Todas as barras do gráfico são o mesmo azul. Pintar cada barra
de uma cor diferente ("porque fica bonito") é erro: a cor passa a dizer o que o tamanho da barra
já disse, e some o único canal livre que você tinha. Cor só vira informação quando há **duas ou
mais séries de verdade**.

**QUANDO HOUVER SÉRIES DE VERDADE — estas cinco, nesta ordem, e mais nenhuma.** Elas foram
**validadas no fundo escuro do niX** (luminosidade, croma, contraste e separação pra quem não
enxerga cor — `scripts/validate_palette.py` da skill `painel`). Não invente outra sem rodar o
validador:

```
--s1:#3987e5  azul     --s2:#199e70  verde-água   --s3:#c98500  amarelo
--s4:#9085e9  violeta  --s5:#d95926  laranja
```
Passou de 5 séries, o problema não é a paleta: **junte o resto em "outros"** ou vire tabela.

**AS TRÊS CORES DE SINAL — e elas NUNCA são cor de série.**
```
--ok:#33c06f    positivo (subiu, em dia, aprovado)
--erro:#ff5b4d  negativo (caiu, vencido, reprovado)
--aviso:#f0b429 atenção (parado, aguardando decisão)
```
Medidas no fundo do niX: **7,4 · 5,7 · 9,4** de contraste (o mínimo de texto é 4,5). Verde é
**significado**, não decoração: barra de gráfico verde "porque ficou bonito" queima a única cor que
a pessoa lê como "está bom". E **estado nunca é só cor** — cor **e** palavra.

**Texto nunca veste a cor do dado.** Rótulo, valor e legenda são `--tinta / --tinta2 / --tinta3`.
Quem carrega a identidade é a **marca colorida ao lado**, não a letra pintada.

**Nunca ponha fundo colorido atrás de texto** — no outro tema vira letra escura em fundo escuro.

**ZERO EMOJI.** Ícone é **SVG de traço** com `stroke="currentColor"`.

### A tipografia — Inter, e uma escala só
| Peça | Tamanho | Peso |
|---|---|---|
| Número herói (um por tela) | 52px (44 no celular) | 600 |
| Valor de azulejo (KPI) | 24px | 600 |
| Título da tela | 19px | 600 |
| Texto e número de tabela | 14–15px | 400 |
| Rótulo, cabeçalho de coluna | 11–13px | 500 |
| Nota, rodapé | 11–12px | 400 |

- **`letter-spacing:-.02em` em número grande.** Sem isso o número parece frouxo.
- **`font-variant-numeric:tabular-nums` SÓ em coluna de número** (tabela, eixo). **Nunca no
  número herói** — dígito de largura fixa faz "121" parecer esparramado em 52px.
- **Título é sentence case.** "Vendas de concreto", nunca "VENDAS DE CONCRETO". Caixa-alta só
  em cabeçalho de coluna (11px, `letter-spacing:.06em`).
- **Não use serifa nem fonte de enfeite** no número. Mesma fonte de tudo.

### O espaço
- Cartão: `border-radius:12px`, `padding:16–20px`, borda `1px solid --linha`.
- Entre cartões: `14px`. Dentro do cartão, entre rótulo e valor: `2–4px`.
- Linha de tabela: `min-height:52px` no PC, `60px` no celular (dedo).
- **Teto de largura:** `max-width:1180px; margin:0 auto`. Nem esticar até a borda, nem faixa magra.

### O número
- **Alinhado à DIREITA**, sempre. Nome/texto à esquerda. Não existe número centralizado.
- Formato brasileiro: `toLocaleString('pt-BR')`. m³ com 1 casa (`9.685,0`), viagens inteiro
  (`1.180`), R$ sem centavos no painel (`R$ 489`), milhão abreviado no azulejo (`R$ 11,0 mi`).
- **Unidade vira `<small>` do lado, não repete em toda linha.** `23.209 m³` — o "m³" pequeno e
  cinza; a coluna já se chama m³, as células não precisam repetir.

### A barra de proporção
- **Trilho** = o mesmo azul a ~22% + **preenchimento** = azul cheio. Altura 6px, raio 3px.
- **Barra minúscula não encoda nada.** Se não sobrar pelo menos ~50px de trilho, **não faça
  trilho**: faça um **traço proporcional na base da linha inteira** (`.lin::after` com
  `width:var(--pc)`). É o que salva o celular — a linha inteira vira a régua.
- A % é **do nível de cima** (do pai), não do total. Diga isso no cabeçalho: "% do nível".

### O gráfico (quando tiver)
- Barra ≤ **24px** de largura, **canto de cima arredondado 4px**, base reta no zero. Nunca
  encher o slot inteiro — o vão faz parte.
- Grade: **1px, sólida, cinza um passo acima do fundo**. Nunca tracejada.
- **Uma escala só.** Nunca dois eixos Y — ver as cinco proibições da Parte 2.
- **Sem dado de tempo, não invente um eixo de tempo.** Sem série temporal, o certo é barra
  ou nenhum gráfico — os azulejos e a tabela bastam.

### Foco e toque
- `:focus-visible{outline:2px solid var(--azul);outline-offset:2px}` em tudo que clica.
- Linha que abre: `tabindex="0"`, `role="button"`, responde a Enter e Espaço.
- Alvo de toque ≥ 40px de altura.

---

## PARTE 4 — O ESQUELETO (copie e adapte)

```html
<!doctype html><html lang="pt-br"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Vendas de concreto</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root{--fundo:#0B0F14;--surf:#151A21;--surf2:#0F141B;--linha:#242B35;--linha2:#1B212A;
      --tinta:#EDF1F6;--tinta2:#93A0B4;--tinta3:#5F6B7C;--azul:#2F7DF6;
      --azul-trilho:rgba(47,125,246,.22)}
*{box-sizing:border-box;margin:0;padding:0}
body{background:var(--fundo);color:var(--tinta);padding:20px 16px 56px;
     font:400 15px/1.45 Inter,system-ui,sans-serif;-webkit-font-smoothing:antialiased}
.folha{max-width:1180px;margin:0 auto}
.cart{background:var(--surf);border:1px solid var(--linha);border-radius:12px}

/* herói: UM por tela, proporcional (sem tabular-nums) */
.heroi{padding:20px 22px;margin-bottom:14px}
.heroi .rot{color:var(--tinta2);font-size:13px}
.heroi .val{font-size:52px;font-weight:600;letter-spacing:-.03em;line-height:1.05}
.heroi .val small{font-size:22px;font-weight:500;color:var(--tinta2);margin-left:4px}

/* azulejos: divisória interna, não cartão solto */
.azulejos{display:grid;grid-template-columns:repeat(4,1fr);overflow:hidden;margin-bottom:14px}
.az{padding:16px 18px;border-left:1px solid var(--linha2)} .az:first-child{border-left:0}
.az .rot{color:var(--tinta2);font-size:12px;margin-bottom:4px}
.az .val{font-size:24px;font-weight:600;letter-spacing:-.02em}

/* a tabela é uma GRADE, não <table>: no celular a coluna some, não encolhe */
.cab,.lin{display:grid;grid-template-columns:minmax(0,1fr) 104px 120px 92px;
          column-gap:12px;align-items:center;padding:0 18px}
.cab{padding:12px 18px;border-bottom:1px solid var(--linha);color:var(--tinta2);
     font-size:11px;font-weight:500;letter-spacing:.06em;text-transform:uppercase}
.lin{min-height:52px;border-bottom:1px solid var(--linha2);position:relative}
.lin.abrivel{cursor:pointer}
.lin.abrivel:hover{background:rgba(255,255,255,.028)}
.lin:focus-visible{outline:2px solid var(--azul);outline-offset:-2px}
.num{text-align:right;font-variant-numeric:tabular-nums;font-size:14px;white-space:nowrap}
.num.fraco{color:var(--tinta2)}
/* nível = peso + recuo. NUNCA cor. */
.n0 .nome{font-weight:600} .n1 .nome{font-weight:500;padding-left:22px}
.n2 .nome{padding-left:44px} .n3 .nome{padding-left:66px;color:var(--tinta2)}
.n1,.n2,.n3{background:rgba(0,0,0,.16)}
.prop{display:flex;align-items:center;gap:8px;justify-content:flex-end}
.prop .trilho{flex:1;max-width:56px;height:6px;border-radius:3px;background:var(--azul-trilho);overflow:hidden}
.prop .ench{height:100%;border-radius:3px;background:var(--azul)}
.meta{display:none}

/* CELULAR: zero rolagem lateral. A coluna principal FICA. */
@media(max-width:720px){
  .cab,.lin{grid-template-columns:minmax(0,1fr) 82px 46px;padding:0 14px}
  .lin{min-height:60px}
  .op{display:none}                       /* colunas secundárias saem da grade */
  .meta{display:block;color:var(--tinta3);font-size:11px}  /* e voltam como linha de apoio */
  .prop .trilho{display:none}             /* trilho de 30px não encoda nada... */
  .lin::after{content:'';position:absolute;left:0;bottom:0;height:2px;
              width:var(--pc,0%);background:var(--azul);opacity:.85}  /* ...vira traço na base */
}
</style></head>
<body><div class="folha">
  <div class="cart heroi">
    <div class="rot">Volume no período</div>
    <div class="val" id="hM">—<small>m³</small></div>
  </div>
  <div class="cart">
    <div class="cab">
      <div>Usina › Cliente</div><div class="num">m³</div>
      <div class="num">% do nível</div><div class="num op">Viagens</div>
    </div>
    <div id="corpo"><div style="padding:32px;text-align:center;color:var(--tinta2)">carregando…</div></div>
  </div>
</div>
<script>
const f1=v=>v.toLocaleString('pt-BR',{minimumFractionDigits:1,maximumFractionDigits:1});
const f0=v=>v.toLocaleString('pt-BR',{maximumFractionDigits:0});
const esc=s=>String(s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
// a linha carrega a própria proporção em --pc (o celular usa pra desenhar o traço da base)
function linha(g,nivel,pai,filho,aberto){
  const pc = pai>0 ? g.m3/pai*100 : 100;
  return `<div class="lin n${nivel} ${filho?'abrivel':''}" style="--pc:${pc.toFixed(1)}%"
     data-k="${esc(g.chave)}" ${filho?'tabindex="0" role="button"':''}>
    <div class="nome">${esc(g.nome)}
      <span class="meta">${f0(g.v)} viagens</span></div>
    <div class="num">${f1(g.m3)}</div>
    <div class="num prop"><span class="trilho"><span class="ench" style="width:${Math.min(100,pc)}%"></span></span>
      <span>${pc.toFixed(0)}%</span></div>
    <div class="num fraco op">${f0(g.v)}</div>
  </div>`;
}
// ERRO NÃO VIRA ZERO. Zero é um valor plausível — e mente.
async function carrega(){
  try{
    const r = await fetch('/api/meu/dados/producao-dia');  // A TOMADA (Parte 0) — dado ao vivo
    if(!r.ok) throw new Error(r.status);
    const D = await r.json();          // { colunas, linhas, quando, do_cache }
    desenha(D);
  }catch(e){
    document.getElementById('hM').textContent='—';
    document.getElementById('corpo').innerHTML=
      '<div style="padding:32px;text-align:center;color:var(--tinta2)">não consegui buscar o dado agora.</div>';
  }
}
carrega();
</script></body></html>
```

**Os sete modelos, prontos pra copiar** (barras, ranking horizontal, empilhada, meta × realizado,
mapa de calor, alertas, tooltip e "ver tabela"): abra a skill `painel` →
`referencias/modelos.md`. Não redigite o que já está escrito lá.

---

## PARTE 5 — O CELULAR (é aqui que os painéis morrem)

**A regra:** no estreito, **coluna secundária SOME da grade e volta como linha de apoio** debaixo
do nome (`.meta`). O que **nunca** sai: o nome, a **medida principal** e a **%**.
O painel inteiro vira **coluna única** e os filtros deslizam de lado (só eles).

| No PC (1440px) | No celular (390px) |
|---|---|
| Nome · m³ · % · Viagens · m³/viagem · R$/m³ | Nome · **m³** · % |
| barra de proporção na coluna | traço proporcional na base da linha |
| — | viagens / m³/viagem / R$/m³ viram uma linha cinza de 11px sob o nome |

**Proibido:** `min-width` em tabela + `overflow-x:auto` como "solução de celular". Isso é
esconder o dado atrás de um gesto que ninguém faz.

**A ÚNICA EXCEÇÃO — a MATRIZ (modelo 8).** Quando **todas** as colunas são necessárias de verdade
(dia × receita × despesa × saldo × acumulado), a rolagem lateral é legal — **e só com as três
muletas juntas**: **1ª coluna congelada** (`position:sticky;left:0` **com fundo opaco**),
**cabeçalho fixo** e **total no pé**; mais o **trio de resumo antes da tabela**, porque ninguém
soma coluna de cabeça. Sem as muletas, é `overflow-x` disfarçado — e o dado sumiu.

Confira medindo, não olhando:
```js
document.documentElement.scrollWidth > document.documentElement.clientWidth  // tem que dar false
```

---

## PARTE 6 — AS ARMADILHAS (todas já aconteceram)

- **Somar preço unitário.** `SUM(VALORVENDA)` → "R$ 60/m³" em vez de R$ 476/m³. Média é
  divisão de somas.
- **Somar m³ de bomba/adicional.** Bomba e adicionais têm R$, **não têm m³**. Filtre
  `TIPODESERVICO='CONCRETO'` pro volume. E a São José (aliança 201) é **cimento**, não entra.
- **Empresa consolidada (código terminado em 99)** repete as filiais e **dobra o número**.
- **Caminho relativo de arquivo/imagem.** `logo.png` vira 404 e a tela nasce cega.
  Sempre com barra no começo (`/minha-tela/logo.png`). O DADO não é arquivo: é a tomada
  (`/api/meu/dados/<nome>`, Parte 0).
- **Tela zerada.** Dado que não veio vira `—` e uma frase, **nunca `0`**. Zero é plausível: ela
  lê "não vendi nada" e decide errado em cima de um defeito seu.
- **Mexer no `app.py` que a pessoa usa.** Sua rota entra **acima** do `app.run(...)`; o que vem
  depois nunca é lido. E as rotas antigas têm que continuar respondendo.
- **Barra de proporção pequena demais.** 30px de trilho: 98% e 4% ficam iguais. Meça ou troque
  pelo traço da base.
- **Nome cru do banco em CAIXA ALTA** ("LUZ BELA EMPREENDIMENTOS IMOBILIARIOS LTDA") ocupa a
  linha toda e grita. Em cliente/obra, passe pra Capitulado.
- **Entregar link que não abre.** Abra o endereço **público** (`https://<você>.api.nix.ia.br/...`)
  e confira o **200** antes de mandar. Já aconteceu de o painel estar pronto e o link estar morto.
- **🔴 DOIS ENDEREÇOS PRO MESMO PAINEL — o pior de todos.** O painel tem **um** lugar: **o menu
  dela**. Publique com **`mcp__nix__publica_tela`** (slug, nome, caminho) — ela confere que a
  oficina serve mesmo aquele endereço e põe no menu — e depois **diga só onde está**: *"está no
  seu menu, em Painel das Obras"*. **Não jogue link no chat**: o item do menu já é o caminho, e o
  link solto vira um segundo endereço pra mesma coisa (lá ele abre em tela cheia; e o do menu
  também — é a mesma tela). Endereço público, só se ela pedir pra **mandar pra alguém de fora**.
  **Nunca escreva `.nexum/telas.json` na mão.** *Aconteceu: o link do chat abria um painel novo e
  o menu continuava apontando pro painel velho; a pessoa clicava no menu e só via erro.*

---

## PARTE 7 — ANTES DE DIZER "ESTÁ PRONTO"

- [ ] Sei **qual pergunta** esta tela responde, e escolhi o **modelo** da Parte 2 por causa dela.
- [ ] **Todo número importante tem comparação** (meta, período anterior ou média) — nenhum número solto.
- [ ] O **rodapé diz a fonte e a hora** ("fonte: X · gerado 09:42") e o que ficou de fora.
- [ ] O **erro tem saída**: frase + botão "Tentar de novo". Nenhum zero mentiroso, nenhum "carregando…" eterno.
- [ ] Nenhuma paginação. Se dá pra **agir** na tela, o botão está lá — e escrita no ERP passa pelo código de 4 dígitos.
- [ ] **Publiquei com `mcp__nix__publica_tela`** e entreguei **o link que ela devolveu**.
- [ ] Abri no celular (390) e no PC (1440), **e cliquei pra abrir um nível** em cada um.
- [ ] `scrollWidth === clientWidth` no celular (zero rolagem lateral).
- [ ] A medida principal aparece **sem arrastar** no celular.
- [ ] Todo gráfico tem **tooltip** e um **"ver tabela"**. Duas séries ou mais: **legenda**.
- [ ] Nenhuma pizza. Nenhum eixo duplo. Nenhum estado codificado só por cor.
- [ ] A cor de cada entidade veio do **mapa fixo**, não do índice do laço.
- [ ] Conferi UM número na fonte (rodei a query do total e bati com o herói).
- [ ] Nenhuma média foi feita como média de média.
- [ ] Zero emoji. Botão em frase. Números à direita, `tabular-nums` na tabela.
- [ ] Erro aparece como frase, não como zero.
- [ ] O rodapé diz **de onde veio o dado** e **o que ficou de fora**.

---

---

## O que a casa aprendeu (com o uso)

*Sai do uso de verdade: o que um Nix descobriu sobre a empresa, conferido e reescrito, vale pra todos.*

- **Programação de pedidos**: um pedido por linha; hora prevista é `BH_CCTITE.HRPREVISTA` (formato HHMM; valores abaixo de 100 indicam cadastro sem hora). M³ carregado soma remessas `F`/`R` de `BH_CCTREM` pelo pedido. Janela padrão: hoje + 7 dias; se o volume ultrapassar ~400 linhas, divida a consulta em blocos. Atualização automática de hora em hora das 6h às 19h, segunda a sábado.
- **Filtro de painel**: use botões de marcar (chips) em vez de `<select>`. O usuário precisa marcar vários valores (ex.: duas usinas, duas situações). Implemente com um `Set` por dimensão e uma função que casa o pedido com os filtros marcados, podendo ignorar uma dimensão. Cada grupo de botões deve mostrar os valores respeitando os demais filtros ativos, com um botão "Todas as..." que limpa aquele grupo.
- **Painel de vendas por motorista**: usa a view `AD_VGF_VENDAS` com filtro `TIPODESERVICO = 'CONCRETO'`, agrupado por `NOMEMOTORISTA`, `CODEMPALIANCA` e `DESCRUSINA`. O campo `TEMPOCICLOENTREGA` não deve ser usado, pois seus valores (0,8 a 4,7) não têm unidade confirmada.

## O que eu aprendi (meu caderno)

*Uma linha por coisa, curta.*

- **Tela "ao vivo" (o Diogo pediu, 14/07/26): a oficina NÃO tem rede nem senha de banco** — só um turno meu alcança o Sankhya. O desenho que funciona é: `gera.py` escreve `prog/dados.json` (carimbo `gerado` **com fuso**, `os.replace` atômico) · o HTML relê o JSON a cada 45 s (sem recarregar a página, sem perder o filtro) · o cron chama `robos/atualiza.sh`, que faz `nexum pensa` mandando o cérebro executar o roteiro `prog/ATUALIZAR.md` · um servidor próprio (`servidor.py`, no lugar do `http.server`) aceita `POST /prog/atualizar` e dispara o mesmo robô, que é o botão **"Atualizar agora"** da tela. Testado ponta a ponta: o carimbo virou sozinho.
- **O container roda em UTC e o Diogo, em Brasília (−3).** Carimbo sem fuso faz o "há X min" da tela errar 3 horas, e cron "6h" dispara às 3h da manhã. Grave `datetime.now().astimezone()` e escreva o cron em UTC (9h-22h = 6h-19h daqui).
- **Programação de amanhã confere com a view oficial** `AD_REL_DIARIO_PROGAMANHA` usando `BH_CCTCAB.SITUACAO <> '0'` (fora os cancelados) — medido igual, aliança por aliança. E ela muda de minuto em minuto: em 3 minutos a Super caiu 23 m³.
