Metadata-Version: 2.4
Name: dashpacks
Version: 0.8.0
Summary: Biblioteca compartilhada das stacks Dash
Author: Rike Morais
License: Proprietary
Project-URL: Homepage, https://github.com/rikemorais/dashpacks
Project-URL: Issues, https://github.com/rikemorais/dashpacks/issues
Keywords: dash,observabilidade,jobs
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.12
Description-Content-Type: text/markdown
Requires-Dist: starlette>=0.40
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: ruff>=0.14; extra == "dev"
Provides-Extra: acesso
Requires-Dist: pyjwt>=2.10; extra == "acesso"
Requires-Dist: cryptography>=44; extra == "acesso"

# dashpacks

Biblioteca compartilhada pelas stacks **DashFy**, **DashTube**, **DashTime**,
**DashHealth**, **DashBook** e **DashDog** — e por todas as ferramentas Dash que
vierem. Serão catorze.

## Por que existe

As quatro stacks nasceram de um monorepo e mantinham uma cópia da camada
`common` cada uma. A medição de 2026-09-08 mostrou o resultado: dos 34 arquivos
dessa camada, **19 eram byte a byte idênticos nas quatro**, e outros 9 divergiam
apenas porque uma stack ficara para trás.

As cópias não evoluíram em direções diferentes. Ficaram paradas em versões
diferentes do mesmo código — e três defeitos vieram daí, todos na mesma semana:

- a correlação por `request_id` não chegava ao log **nas quatro stacks**
- as regras de exclusão de chave estrangeira divergiram com o modelo idêntico
- a redefinição administrativa de senha foi portada três vezes à mão

A decisão e o levantamento estão no ADR 0003 do DashTube.

## O que entra aqui

**A regra é não depender do que fica nas stacks.** Ser idêntico nas quatro não
basta: três serviços eram idênticos e não puderam vir nesta fase, porque
importam os modelos de usuário.

Hoje:

| módulo | o que faz |
|---|---|
| `dashpacks.observability.context` | `request_id` e `job_id` em `ContextVar` |
| `dashpacks.observability.logging` | formato JSON com os identificadores |
| `dashpacks.observability.middleware` | fixa o `request_id` por requisição |
| `dashpacks.observability.job_metrics` | métricas dos trabalhos de fundo |
| `dashpacks.services.jobs` | fila serializada de trabalhos |
| `dashpacks.services.job_registry` | registro dos trabalhos disponíveis |
| `dashpacks.services.password_policy` | validação de força de senha |

## Como usar

No `requirements.txt` da stack, **com versão fixa**:

```
dashpacks @ git+ssh://git@github.com/rikemorais/dashpacks@v0.1.0
```

A versão é fixada por tag de propósito: nenhuma stack sobe sozinha, e a
atualização passa por PR e CI em cada uma. É o que impede uma versão ruim de
alcançar as quatro de uma vez.

```python
from dashpacks.observability.logging import configure_logging
from dashpacks.observability.middleware import RequestContextMiddleware

# Depois das migrações, não antes: o fileConfig do Alembic reinstala o
# formatador da raiz, e o efeito é silencioso.
configure_logging(json=settings.log_json, level=settings.log_level)
```

`configure_logging` recebe a configuração por parâmetro em vez de importar um
`settings`. São quatro aplicações com configurações próprias, e ler a de uma
delas aqui dentro amarraria as quatro ao mesmo objeto.

## O pacote de frontend

O mesmo repositório traz um pacote npm, em `js/`, com o que é comum ao frontend
das quatro stacks. A regra de entrada é a mesma: **não pode depender do que fica
nas stacks**, e ser idêntico nas quatro não basta.

| módulo | o que faz |
|---|---|
| `usePolling` | consulta periódica com cancelamento |
| `PollingController` | o controlador por trás dela, testável sem React |
| `onSessionExpired` | avisa a aplicação quando a sessão cai |
| `flagOf` | bandeira do país a partir do código |
| `titleCasePt` | capitalização que respeita as preposições do português |

O que é tema, cor e marca de cada produto **não** mora aqui: entra por
propriedade.

```
npm install @rikemorais/dashpacks@0.3.4
```

**Do registro público do npm, e não do GitHub.** O npm 12 desabilitou por padrão
as duas alternativas — `allow-git` e `allow-remote` vêm como `"none"` —, e
instalar da release ou do repositório recusa com `EALLOWREMOTE` e `EALLOWGIT`.

Ligar essas opções resolveria, e foi descartado: elas valem para **todas** as
dependências do projeto, não só para esta, e existem para barrar dependência de
origem não verificada.

O GitHub Packages seria a outra saída, e reintroduz o problema que o lado Python
evitou: exige token de autenticação nas quatro CIs e na máquina local, mesmo
para pacote público.

## Uma stack nova

A lista abaixo não é sugestão. Cada item está aqui porque a falta dele já custou
alguma coisa em pelo menos uma stack.

- [ ] Consumir a `@rikemorais/dashpacks` no frontend e a `dashpacks` no backend
- [ ] `LoginPage` da biblioteca, com o `id` da ferramenta — nunca uma cópia. A
      tela de login já existiu em seis cópias, separadas por uma linha: o nome
      do produto
- [ ] `LancadorDeAplicativos`, `MarcaDaFerramenta` e `AcoesDeConta` no
      cabeçalho, com Configurações e Perfil no mesmo lugar das outras
- [ ] `ConfiguracoesPage` com a aba Aparência
- [ ] Entrada em `js/src/lib/ferramentas.ts`: id igual ao subdomínio, ícone do
      lucide e cor base
- [ ] Ícones gerados por `scripts/gerar-icones.mjs`, e o favicon **embutido como
      data URI** — o Cloudflare Access devolve 302 no lugar da imagem quando o
      navegador pede o favicon sem sessão
- [ ] Tokens de cor completos no CSS: `--bg`, `--text`, `--surface`,
      `--surface2`, `--border`, `--muted`, `--accent` e `--danger`. Faltando
      algum, o componente compartilhado renderiza sem cor de destaque
- [ ] Rotas de conta no backend: `/auth/password`, `/auth/totp/new` e
      `/auth/totp/confirm`, e **trocar senha ou autenticador derruba as outras
      sessões**
- [ ] Orçamento de desempenho ligado ao `npm run build`, e verificado apertando
      o limite de propósito — não por leitura
- [ ] `.github/dependabot.yml` com `npm` em `/frontend` (diário) e
      `github-actions` na raiz (semanal)
- [ ] Teste que afirma o valor da chave do `localStorage` do tema e da cor base
- [ ] Toda mudança de estado gravando evento em UTC, se o domínio tiver estado
      que muda — ver a seção abaixo

### Duas armadilhas que já pegaram

**O Dependabot lê a branch padrão.** Configuração mergeada em `dev` não vale
enquanto a `main` não a tiver. Já aconteceu: o arquivo estava lá, mergeado, e o
Dependabot não rodava em quatro das seis stacks.

**A chave do `localStorage` não pode mudar de valor.** Ao adotar o tema
compartilhado, é tentador padronizar a chave. Quem já escolheu um tema perde a
escolha em silêncio na primeira visita depois do deploy. O DashDog continua com
`dashdog-theme`, com hífen, por isso.


## Data e hora de toda mudança de estado

Regra da plataforma, decidida no
[ADR 0005](https://github.com/rikemorais/dashtube/blob/main/docs/adr/0005-data-e-hora-de-toda-mudanca-de-estado.md).
Vale para qualquer stack cujo domínio mude de estado.

**Toda mudança que o usuário provoca vira um evento, gravado em UTC, ao lado do
estado corrente.**

O que motivou: no DashFy, o `updated_at` era sobrescrito a cada alteração e a
nota anterior deixava de existir. Uma faixa podia ser cinco estrelas hoje e
quatro amanhã, e essa mudança sumia. Remover a nota apagava a linha, levando
junto o fato de que houve nota um dia.

### Como fazer

- **O estado corrente continua onde está.** A tabela que a tela lê não muda:
  ler o estado atual não pode custar uma varredura de histórico.
- **Uma tabela de eventos por domínio, só de inserção.** Sem `UPDATE` e sem
  `DELETE`: um evento corrigido depois é outro evento.
- **Cada evento carrega o tipo da ação.** Sem ele, ações diferentes sobre o
  mesmo registro ficam indistinguíveis — no DashFy, favoritar e reavaliar
  produziriam linhas idênticas.
- **Remover um valor também é evento**, com o campo nulo.
- **Eventos bloqueiam a exclusão do usuário**, como o estado que descrevem. Se
  a nota impede a exclusão, a história dela não pode sumir sozinha.

### O tempo

`DateTime(timezone=True)` e `datetime.now(UTC)`. **Nada de horário local
persistido** — ele é irreversível, porque não dá para recuperar o instante que
representava depois que o fuso muda.

A API **não converte**: devolve o instante com o deslocamento explícito. O
servidor não sabe onde quem lê está.

**Séries temporais não são agregadas no servidor.** Um evento das 23h em UTC
pode ser das 20h do mesmo dia em São Paulo, e o agrupamento por dia mudaria de
balde conforme o fuso. Quem sabe o fuso é a tela — use `diaDoInstante` daqui.

### O que a biblioteca oferece

| símbolo | para quê |
|---|---|
| `criarFuso(chave)` | a escolha do usuário, por ferramenta e por navegador |
| `formatarInstante(iso, fuso)` | escrever um instante UTC no fuso de exibição |
| `diaDoInstante(iso, fuso)` | o dia `AAAA-MM-DD` para agrupar séries na tela |
| `<Aparencia fuso={fuso} />` | o seletor, na aba Aparência das Configurações |

### Ao adotar numa stack que já tem dados

O histórico começa quando a tabela de eventos passa a existir: **evento não se
inventa para trás.** Onde houver `updated_at`, ele pode semear um primeiro
evento — desde que o tipo diga que é semente. Ele representa a última mudança,
não todas, e ler o histórico achando que é completo desde o começo seria pior
que não tê-lo.


## Permissões, vindas do DashPass

Regra do
[ADR 0007](https://github.com/rikemorais/dashtube/blob/main/docs/adr/0007-permissoes-no-dashpass.md):
o DashPass administra quem acessa o quê, e publica um documento assinado. Cada
stack **busca** esse documento e guarda a última versão válida.

```python
from dashpacks.acesso import LeitorDePermissoes

permissoes = LeitorDePermissoes(
    url_do_documento="https://dashpass.rikemorais.com/api/permissoes",
    url_das_chaves="https://dashpass.rikemorais.com/api/permissoes/chaves",
    ferramenta="dashfy",
)

if not permissoes.pode_entrar(email):
    raise HTTPException(403, "Sem acesso a esta ferramenta.")
```

Requer o extra: `pip install dashpacks[acesso]`.

### Três propriedades, e por que elas importam

- **Nenhuma requisição consulta o DashPass.** A cópia local responde; o
  intervalo padrão é de cinco minutos.
- **O DashPass fora do ar não tira acesso de ninguém.** A última cópia continua
  valendo, e a falha é registrada em vez de virar recusa.
- **Uma stack que nunca leu o documento recusa.** Liberar por falta de
  informação é o modo de falha perigoso.

O custo declarado: **revogar não é instantâneo.** Vale a partir da próxima
leitura, e o intervalo é o tamanho dessa janela.


## Versão

**Os dois pacotes compartilham a versão do repositório.** Uma tag `vX.Y.Z`
publica os dois, mesmo que só um tenha mudado.

Uma consequência aprendida na prática: **uma versão publicada e depois
despublicada no npm fica queimada para sempre** — o registro recusa publicar
sobre ela com `You cannot publish over the previously published versions`. Foi o
que aconteceu com a `0.2.0`, e a saída foi subir para a `0.3.0`. Não há como
reaproveitar o número.

A alternativa seria versionar cada um por conta própria, com tags `py-` e `js-`.
Foi descartada porque dobra o que é preciso lembrar na hora de publicar, e o
custo de subir um pacote que não mudou é zero: quem consome fixa a versão e só
atualiza quando quiser.

Consequência prática: a `v0.2.0` traz o pacote npm novo e o Python **sem
mudança de código** desde a `v0.1.0`. Uma stack que só usa o Python não tem
motivo para atualizar.

## Publicar uma versão

```
git tag -a v0.2.1 -m "v0.2.1" && git push origin v0.2.1
```

Depois, anexar os dois artefatos à release: a wheel do Python (`python -m build
--wheel`) e o tarball do npm (`npm pack --workspace js`).

Depois, subir a versão no `requirements.txt` de cada stack, por PR. O atraso de
uma stack fica visível no arquivo, em vez de invisível na cópia.
