Skip to content

Latest commit

 

History

200 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Gestão de Supermercados — GitHub Pages

Versão da aplicação: v3.23.0 URL de produção: https://qzte.github.io/supermercados/


⚠️ Ler primeiro: o index.html é gerado

Não editar index.html à mão. É produzido a partir de src/index.src.html e qualquer alteração directa é perdida no build seguinte. O ficheiro que se edita é o src/index.src.html.

Estrutura do repositório

Os ficheiros servidos estão todos na raiz — a app faz fetch() de caminhos relativos sem subpastas. As fontes e as ferramentas ficam à parte.

supermercados/
│
│  ── EDITAR ────────────────────────────────────────────────
├── src/index.src.html                  # ✏️  a aplicação (contém JSX)
├── tools/build.mjs                     #     compila o JSX
├── workflow-default.json               #     template do workflow — 7 fases
├── email-template.json                 #     templates de e-mail  ⚠ minúsculas
├── supermercados_backup.json           #     dados dos processos
│
│  ── TERCEIROS — substituir, não editar ───────────────────
├── xlsx.full.min.js                    # 📦  SheetJS 0.20.3 (hash fixado no validate)
│
│  ── GERADO — não editar ──────────────────────────────────
├── index.html                          # 🤖  aplicação servida
├── supermercados_<versão>.html         # 🤖  cópia arquivada (idêntica)
│
├── .github/workflows/build.yml         #     compila e faz commit no push
├── .github/workflows/ci.yml            #     valida os pull requests
├── tools/validate.mjs                  #     valida os ficheiros servidos
├── tools/lint.mjs                      #     lint do código da aplicação
├── tests/harness.mjs                   #     carrega a app num contexto Node
├── tests/unit.mjs                      #     testes sem browser (node:test)
├── tests/browser.mjs                   #     testes em Chromium (Playwright)
├── package.json                        #     dependências do build
├── .nojekyll                           #     desativa o processamento Jekyll
└── README.md                           #     este ficheiro

⚠️ Case-sensitive. O GitHub Pages distingue maiúsculas de minúsculas. O ficheiro tem de se chamar email-template.jsonEMAIL-template.json devolve 404 e a app cai no fallback inline.

.nojekyll é obrigatório — sem ele o Jekyll ignora ficheiros iniciados por _ e pode interferir com o servir dos JSON.


Deploy inicial

  1. Criar o repositório supermercados na conta qzte (público).
  2. Copiar todos os ficheiros acima para a raiz do repositório.
  3. SettingsPagesSource: Deploy from a branchBranch: main / / (root).
  4. Aguardar 1–2 min pela primeira publicação.
  5. Abrir https://qzte.github.io/supermercados/

Actualizar a aplicação

  1. Editar src/index.src.html.
  2. Ao mudar de versão, actualizar os dois sítios onde ela aparece — const APP_VERSION e o badge no cabeçalho. O build recusa-se a correr se divergirem.
  3. Commit + push. A Action compila e faz commit do index.html e da cópia versionada; o Pages republica.

O nome do ficheiro arquivado deriva de APP_VERSION, e a cópia da versão anterior é removida automaticamente — deixa de ser preciso fazê-lo à mão.

Podes editar src/index.src.html pela interface web do GitHub. A Action trata do resto; o index.html aparece num segundo commit, cerca de um minuto depois.

Build local (opcional)

npm ci
npm run build        # gera index.html + cópia versionada
npm run check        # verifica se estão actualizados, sem escrever
npm run validate     # valida os ficheiros JSON servidos
npm run lint         # lint do código da aplicação (regras de bugs, não de estilo)
npm test             # testes unitários (~1 s, sem dependências novas)
npm run ci           # validate + lint + test + check (segundos)
npm run test:browser # testes em Chromium — instala o Playwright, leva minutos
npm run ci:full      # ci + testes de browser — a mesma cobertura que o CI dá em dois jobs

npm run check é o que evita que o index.html fique dessincronizado da fonte.


Integração contínua

Duas Actions, com papéis separados de propósito:

Workflow Quando corre O que faz Permissões
build.yml push que toque em src/, tools/ ou package*.json compila e faz commit dos ficheiros gerados contents: write
ci.yml todos os pull requests e push para main apenas verifica; não escreve nada contents: read

O ci.yml existe para fechar uma lacuna concreta: uma alteração só a workflow-default.json ou a email-template.json não aciona o build.yml, porque não está nos caminhos do gatilho. Eram por isso os únicos ficheiros a chegar a main sem verificação nenhuma — e são precisamente os que falham em silêncio, porque a app cai num fallback inline em vez de dar erro. O workflow-default.json já esteve meses em esquema legado, servido mas nunca lido, sem ninguém dar por isso.

O que o ci.yml verifica:

  1. npm run validate — os três ficheiros que a app vai buscar por fetch(): nome exacto (o Pages é case-sensitive), JSON válido, ids de fase e de passo únicos, campos obrigatórios dos templates de e-mail, e a referência cruzada entre os dois — um emailKey sem template correspondente não dá erro nenhum em runtime, o botão "Ver template de e-mail" simplesmente não faz nada. Valida ainda o envelope do backup e os ícones do manifest.
  2. npm run lint — extrai os dois blocos <script> de src/index.src.html e aplica regras que detectam bugs, não estilo: entre elas o react-hooks/exhaustive-deps, que apanha sozinho a classe de erro dos PRs #76 e #77 (dependência em falta num useCallback), e a no-dupe-keys, que já encontrou uma chave cursor repetida num botão. Ver Lint abaixo.
  3. npm run build — compila o JSX. Falha se o JSX estiver partido, se APP_VERSION e o badge do cabeçalho divergirem, ou se o workflow-default.json estiver em esquema legado.
  4. npm test — os testes unitários (ver abaixo).
  5. npm run check — só em PRs vindos de forks, onde o build.yml não corre e os ficheiros gerados têm de vir já actualizados no próprio PR. Em PRs deste repositório a verificação seria uma corrida com o commit do bot, que sincroniza os ficheiros um minuto depois.

Correr npm run ci localmente reproduz os pontos 1, 2, 3 e 5.

Num segundo job, separado, correm os testes de browser. A separação é deliberada: o Validar responde em segundos e é o que dá o sinal rápido em cada push; o de browser descarrega um Chromium e leva minutos. Juntá-los faria toda a gente esperar pelo lento para saber o resultado do rápido.


Lint

npm run lint (tools/lint.mjs). O código vive dentro de src/index.src.html, em dois blocos <script>, e o ESLint não lê HTML — a ferramenta extrai os blocos e linta-os como ficheiros soltos. O bloco JSX é isolado pela mesma expressão que o tools/build.mjs usa: lintar algo diferente do que é compilado não provaria nada sobre o que vai para produção. As linhas comunicadas são as do src/index.src.html, já com o deslocamento somado.

Regras de bugs, não de estilo. Um preset completo traria centenas de avisos sobre código que já funciona, e um lint que ninguém lê é pior do que nenhum. Cada regra foi escolhida por detectar um defeito real e medida contra o código actual antes de entrar — o lint nasceu a zero achados, sem baseline nem excepções herdadas, por isso qualquer problema novo é mesmo novo.

O que já apanhou:

Regra Achado
react-hooks/exhaustive-deps A classe de erro dos PRs #76 e #77 — dependência em falta num useCallback. Ambos foram encontrados à mão; é para não ser preciso repetir isso que o lint existe.
no-dupe-keys Uma chave cursor repetida no botão de exportar Excel: a primeira era código morto, e os botões CSV e PDF ao lado já usavam a forma correcta.

require-atomic-updates foi considerada e ficou de fora: dava 5 achados, todos falsos positivos (e.target.value e iframe.style depois de um await), porque não distingue estado partilhado de um nó do DOM.

Ao acrescentar uma regra

Medir primeiro. Se der achados, ou se corrige o que ela encontrou no mesmo passo, ou a regra não entra — nunca uma lista de excepções, que é como um lint deixa de ser lido.


Testes

Duas camadas, porque provam coisas diferentes.

tests/unit.mjs tests/browser.mjs
Corre com node:test nativo Playwright + Chromium
Dependências novas nenhuma Playwright, instalado à parte
Duração ~1 s minutos
Alvo as funções de src/index.src.html o index.html construído
No CI job Validar job Testes de browser
Localmente npm run ci npm run test:browser

Os dois juntos são o npm run ci:full. O npm run ci fica deliberadamente rápido — é o que se corre a cada alteração; o ci:full é o que se corre antes de abrir um PR.

Os testes correm o código real, não uma cópia. O tests/harness.mjs extrai os dois blocos <script> da aplicação, compila o JSX com o mesmo Babel do build e avalia-os num contexto vm. Uma cópia da lógica diverge da fonte sem avisar, e um teste que valida uma cópia não prova nada sobre o que é publicado.

Um teste existe por uma de duas razões, e escreve no comentário qual é.

  1. Corresponde a uma falha que já aconteceu aqui — documentada no SECURITY_AUDIT.md, neste ficheiro ou num comentário do código. Foi durante muito tempo o único critério, e continua a ser o principal: é a resposta ao achado A10, em que correcções de segurança deste repositório se perderam duas vezes por substituição de ficheiro.
  2. É a rede de uma reestruturação planeada. O KpisPanel (1200 linhas), o ProcessDetail (890) e o App (650) estão para ser partidos em componentes, e o editor de workflow imperativo para ser absorvido pelo React. Nada disso se faz com segurança sobre código que nenhum teste renderiza: um erro de render não falha nada — o React desmonta a árvore e a vista fica em branco, sem mensagem. Os grupos que percorrem as três vistas, os filtros, a ida e volta do backup, o refresh da sessão e o Plano Anual existem para isso.

O que continua a não haver é teste escrito por cobertura. Um teste que não saiba dizer que falha impede — passada ou futura — não entra.

Porquê duas camadas, e não só uma

O Node não tem DOMParser, e o sanitizeRichText — a defesa central do achado A2 — depende dele: sem parser cai num ramo de recurso. Um teste de payloads em Node passaria por esse ramo e daria uma garantia falsa sobre o filtro que corre no browser. O harness não injecta um DOMParser de mentira, de propósito; a sanitização de HTML, os templates de PDF e o percurso da interface são cobertos em browser.mjs, contra um parser verdadeiro.

O valor desta separação foi medido: neutralizando o sanitizeRichText (a devolver o HTML sem o filtrar), o npm run build e o npm test continuam os dois a passar — as guardas do build só verificam que a função existe e é chamada, não o que ela faz — e os 9 testes de XSS no browser ficam vermelhos. É exactamente a lacuna que a segunda camada existe para fechar.

Ao acrescentar um teste

Escreve o comentário que diz que falha é que ele impede. Sem isso, o teste seguinte a ficar vermelho por uma razão legítima é apagado por não se perceber o que protegia.

Verifica também que ele falha quando a protecção é removida. Um teste que passa nos dois casos não é rede nenhuma — só ruído com aparência de segurança.

Endurecimento

Aplica-se aos dois workflows:

  • Actions fixadas por SHA, não por tag — uma tag pode ser reapontada para outro commit, e o CI passaria a executar código diferente sem que nada mudasse no repositório. Ao actualizar, trocar o SHA e o comentário com a versão que fica ao lado.
  • npm ci --ignore-scripts — o build precisa do Babel e o lint do ESLint, ambos como bibliotecas.
  • persist-credentials: false no checkout. Sem isto o token fica gravado em .git/config durante todo o job, incluindo enquanto correm o npm ci e o tools/build.mjs — passos que executam código do repositório e das suas dependências.

No build.yml, que é o único que escreve, o token existe apenas no ambiente do passo de commit e o URL vai como argumento do git push (que, ao contrário do git remote add, não grava nada). O refspec é explícito porque, sem credenciais persistidas, não há upstream configurado.

O passo de commit/push esteve muitos meses sem nunca correr — quem edita tem feito sempre o build localmente, pelo que o workflow encontrava "nada a fazer" e saía antes do push. Foi exercitado de propósito em v3.15.6, com um commit que alterava só o src/index.src.html: a Action regenerou o index.html, renomeou a cópia arquivada e fez commit como github-actions[bot], e o resultado ficou byte a byte igual ao de um build local. O caminho está confirmado a funcionar.

Se um dia falhar, falha de forma visível: o passo dá erro e o workflow fica vermelho — nunca deixa os ficheiros gerados dessincronizados em silêncio.


Porquê um build

src/index.src.html contém ~5 500 linhas de JSX, que o browser não percebe. Sem build, cada carregamento de página descarregava o Babel (2,8 MB; 589 KB comprimidos) e traduzia essas linhas outra vez — trabalho idêntico, repetido em cada visita de cada utilizador, com a página em branco enquanto corria.

Tempo até a app estar utilizável (mediana de 3 execuções, bibliotecas locais):

CPU Sem build Com build
Desktop 1 529 ms 140 ms 10,9×
Telemóvel médio (4× mais lento) 4 958 ms 456 ms 10,9×
Telemóvel modesto (6×) 7 044 ms 809 ms 8,7×

Em produção a diferença é maior: o Babel vinha do cdnjs, pelo que desaparecem também 589 KB de transferência. O index.html cresce 28 KB (o JS compilado é mais verboso que o JSX) — troca amplamente favorável.

O ficheiro-fonte mantém o <script type="text/babel"> de propósito: abre e corre directamente no browser, sem passo de build, o que o mantém utilizável para testar alterações antes do commit.


Carregamento automático

Servida por HTTPS, a app resolve os fetch() relativos ao subpath /supermercados/:

Ficheiro Fetch no código Efeito se ausente
workflow-default.json ?nocache= / ?v= fallback inline window.__SUPERMERCADOS_INLINE_DEFAULT
email-template.json ?v= fallback window.__SUPERMERCADOS_INLINE_EMAIL_TEMPLATES
supermercados_backup.json ?v= arranca vazio (BootScreen)

Todos usam cache-busting por querystring — actualizações são apanhadas sem refresh forçado.

Em file:// os fetch() são bloqueados pelo protocolo e a app usa exclusivamente os fallbacks inline.

Aviso de versão nova

Os ficheiros de dados acima são relidos a cada arranque, mas o próprio HTML não: uma página aberta continua a correr a versão com que foi carregada até alguém a recarregar. Como a app fica aberta um turno inteiro, uma publicação nova podia passar horas sem chegar a quem está a usá-la — e o sintoma é enganador (dois postos com números diferentes, uma correcção que "não fez nada").

A app passou a avisar. O bloco avisoDeVersaoNova, no guarda de arranque de src/index.src.html:

Sinal Quando dispara
controllerchange um service worker novo assume uma página já aberta — o estado híbrido em que o HTML em memória é o antigo e quem serve os pedidos seguintes já é o worker novo
sondagem do documento de 15 em 15 minutos, ao voltar ao separador e ao recuperar a ligação: vai buscar o HTML publicado com cache: 'no-store' e compara o badge de versão com o desta página

Hoje a app não regista nenhum service worker, pelo que o primeiro sinal nunca dispara — está lá para que registar um não implique descobrir este problema outra vez. É a sondagem que faz o trabalho.

Quando há versão nova aparece um aviso discreto — «Nova versão disponível (vX.Y.Z)», com «Recarregar» e «Agora não». Nunca recarrega sozinho: deitaria fora o formulário meio preenchido de quem estivesse a escrever. Falhas de rede na sondagem são silenciosas, e o aviso fica por baixo do ecrã de arranque e dos modais, que são modais de propósito.

As duas versões comparadas saem ambas do badge #app-version-badge (o build já recusa construir se ele divergir do APP_VERSION), por isso não há uma segunda fonte de verdade a manter. Renomear esse id, ou perder o listener, desligava o aviso sem erro nenhum — o tools/build.mjs (secção 1g) falha se isso acontecer.

Arranque: escolha explícita, memorizada quando é seguro

Na primeira utilização a app pergunta pelos dois ficheiros — workflow e backup de dados — em vez de aplicar automaticamente o que está no repositório. O ecrã mostra nº de fases/passos e nº de processos e data de cada versão publicada, para a escolha ser informada.

A escolha fica memorizada em localStorage (supermercados:bootChoice), mas o que se guarda é a origem, nunca o conteúdo:

Origem Repetida sem perguntar? Porquê
publicada é relida do repositório em cada arranque — fresca por construção
incluida vem do próprio ficheiro da app
sem-backup não há nada a reler
local um ficheiro do disco não pode ser relido sem nova interacção; guardar uma cópia sua seria precisamente o snapshot desactualizado que este ecrã evita

Ou seja: quem usa as versões publicadas não volta a ser interrompido, e continua a receber dados actuais porque são refetchados. Quem usa um ficheiro local é sempre questionado — não há alternativa correcta. Se a releitura falhar (offline, ficheiro removido, formato mudado), o arranque volta a perguntar em vez de adivinhar.

A barra de topo do Tracking mostra a origem dos dados em title e tem um botão ⇄ trocar que reabre o ecrã de arranque.

A sessão persiste em sessionStorage — um refresh não regressa ao ecrã de arranque; fechar a aba limpa-a. Os processos e o plano anual são ambos guardados nessa sessão.

Publicar um workflow novo

O workflow-default.json é a via para alterar o processo para toda a gente sem tocar no código: editar no editor de workflow, exportar, e fazer commit do ficheiro. O loadWorkflow dá-lhe precedência sobre a versão embutida na app (e o localStorage tem precedência sobre ambos, para não sobrescrever edições locais em curso).

⚠️ O ficheiro tem de estar no esquema actual — fases com id/code/color/label e passos com role/instructions/emailKey. Um ficheiro em esquema legado é ignorado em silêncio: parece estar a ser servido, mas a app usa a versão embutida. Foi o que aconteceu durante meses. O npm run build passa a falhar nesse caso, com a indicação do que corrigir.

Content-Security-Policy

O index.html traz uma <meta http-equiv="Content-Security-Policy"> logo a seguir ao <meta charset>. Tem de ficar aí: uma meta CSP só governa o que aparece depois dela, e o GitHub Pages não permite configurar cabeçalhos HTTP. O npm run build falha se a meta desaparecer, se descer para depois do primeiro <script>/<link>, ou se lhe faltar uma directiva essencial.

Há duas políticas, e é de propósito. A que está no src/index.src.html é a de desenvolvimento: abrir esse ficheiro directamente compila o JSX com o babel-standalone, que precisa de 'unsafe-eval' e do cdnjs. O npm run build reescreve a script-src no index.html publicado, onde o Babel já não existe. Ao alterar uma, alterar a outra.

O que a política publicada entrega:

script-src 'self' 'sha256-…' sem 'unsafe-inline' e sem 'unsafe-eval'. Cada um dos blocos inline é autorizado pelo seu próprio hash, pelo que um <script> ou onerror= injectado não corre
connect-src 'self' corta a exfiltração — é o que um XSS faria a seguir a executar
default-src 'none' nega por omissão tudo o que não esteja enumerado
object-src · base-uri · form-action fechados, todos sem uso aqui

Isto só é possível porque os 30 handlers em atributos passaram a addEventListener (ver ligarControlosEstaticos) e a exportação de PDF deixou de usar eval(). O npm run build falha se qualquer um dos dois voltar — um onclick= no markup ou um eval() no código obrigariam a reabrir a política.

⚠️ Os hashes são calculados pelo build, nunca escritos à mão. Um hash errado não dá erro visível: o bloco é bloqueado e a aplicação não arranca, em silêncio. É por isso que os testes de browser escutam securitypolicyviolation — sem isso, uma CSP partida passava no CI.

Pela mesma razão, um bloco <script> novo no ficheiro-fonte falha o build até ter hash: são esperados exactamente 4 (guarda de arranque, PINs/templates, aplicação, JSX compilado), mais os dois dos templates de relatório, que herdam esta política por serem carregados em srcdoc.

Ao acrescentar uma biblioteca ou uma origem nova, a lista de directivas tem de ser actualizada, senão o recurso é bloqueado em silêncio para o utilizador.

Dependências externas

React, ReactDOM, jsPDF, html2canvas e SheetJS são todas servidas pela própria aplicação (react.production.min.js, react-dom.production.min.js, jspdf.umd.min.js, html2canvas.min.js, xlsx.full.min.js), não por CDN. Sem terceiro capaz de substituir o ficheiro depois de publicado, e a app arranca e exporta PDF/Excel sem rede, incluindo em file://. Se algum destes ficheiros não carregar (falha ao servir, corrupção), a app mostra um ecrã de erro explícito com botão de recarregar — em vez de uma página em branco. O mesmo acontece se o arranque falhar por outro motivo ou demorar mais de 12 s.

O babel-standalone continua a vir de cdnjs.cloudflare.com, mas só é usado ao abrir src/index.src.html directamente em desenvolvimento — o build.mjs remove-o do index.html publicado, pelo que não é uma dependência de produção.

Para actualizar qualquer uma das cinco bibliotecas: npm view <pacote>@<versão> dist.tarball (ou, para o SheetJS, descarregar de https://cdn.sheetjs.com), copiar o ficheiro dist/umd de produção correcto, correr sha256sum <ficheiro> e actualizar version e sha256 da entrada correspondente em tools/validate.mjs no mesmo commit. O npm run validate falha se algum ficheiro e o seu hash divergirem — uma troca silenciosa da biblioteca não passa despercebida. A versão publicada de SheetJS no cdnjs está congelada na 0.18.5, que tem duas vulnerabilidades conhecidas (CVE-2023-30533, prototype pollution; CVE-2024-22363, ReDoS) — o SheetJS saiu do npm e do cdnjs, e as versões corrigidas só existem fora deles.

Precedência do workflow

Se existir uma versão editada em localStorage (workflowTemplate), esta tem precedência sobre o workflow-default.json servido — o editor local não é sobrescrito por uma versão potencialmente mais antiga do repositório.

Precedência dos templates de e-mail

Os templates seguem a mesma regra, e pela mesma razão. O editor grava-os dentro de workflowTemplate ({ emailTemplates, phases }), pelo que a ordem no arranque é:

  1. workflowTemplate.emailTemplates no localStorage — as edições do utilizador;
  2. email-template.json servido pelo repositório;
  3. emailTemplates embutidos no workflow (inline default, usado em file://).

São lidos independentemente das phases: um workflow guardado que seja rejeitado por inválido não leva os templates editados atrás.

Consequência prática: num browser onde já se editaram templates, uma actualização de email-template.json no repositório não é apanhada. É o comportamento pretendido (uma edição local nunca é desfeita em silêncio); para voltar à versão publicada, usar ↺ Repor original no editor de workflow, que limpa o localStorage.


Persistência dos dados

O GitHub Pages é read-only. O supermercados_backup.json no repositório é apenas o estado inicial de leitura.

Fluxo de trabalho:

  1. Abrir a app → dados carregados do repositório.
  2. Entrar → modo Editor → PIN → editar. O PIN é SUPERMERCADO (em window.__SUPERMERCADOS_EDITOR_PINS) e ignora maiúsculas/minúsculas (supermercado = SUPERMERCADO).

O PIN não protege informação. Está em texto simples no ficheiro servido, que é público, e quem quiser contorná-lo contorna-o em segundos. Existe para obrigar a um acto consciente antes de se concluir passos do workflow — fricção deliberada, para que avançar um processo nunca seja um clique distraído. O que protege o estado oficial dos dados é o acesso de escrita a este repositório.

Não o transformar em autenticação: hashear ou cifrar o PIN no cliente é teatro, porque a chave viaja com a fechadura. E não o remover por parecer inútil — o travão é a funcionalidade.

O modo de edição expira uma hora depois de o PIN ser aceite. A contagem é absoluta e não por inactividade, de propósito: quem está a editar sem parar é precisamente quem deve voltar a parar. Expirar não perde trabalho — os processos continuam na sessão e o aviso ● não guardado mantém-se; o que desaparece são os controlos de edição, incluindo o 💾 Guardar backup, que voltam com o PIN. 3. 💾 Guardar backup → ficheiro descarregado localmente. 4. Commit do ficheiro actualizado sobre supermercados_backup.json.

⚠️ Não editar em simultâneo — o último commit sobrescreve.

O backup incluído neste pacote está vazio (version: 2, data: []) — arranque limpo.

Cancelar o diálogo de gravação não conta como gravar. O indicador ● não guardado só desaparece depois de o ficheiro ser mesmo escrito; se o diálogo do browser for fechado sem escolher destino, o aviso mantém-se e o alerta de fecho de página continua armado.

O plano anual viaja dentro do backup. Trocar de ficheiro — pelo ⇄ trocar ou pelo 📂 Carregar backup — substitui o plano pelo do ficheiro novo. Um backup sem plano deixa o plano vazio, em vez de herdar o do ficheiro anterior.

Identificadores de processo

O id de cada processo é a chave usada para gravar e eliminar. Backups gerados por versões antigas podem conter ids repetidos (derivavam de Date.now(), igual para processos criados no mesmo milissegundo). No carregamento, os duplicados são reatribuídos — a primeira ocorrência mantém o id original. Sem isto, editar um processo reescrevia os homónimos e eliminá-lo eliminava-os todos.

Identificadores de fase

O currentPhase de um percurso é um ordinal: nasce a 1, é incrementado de 1 em 1 e é limitado a phases.length. Metade do código lê-o como tal (phases[currentPhase - 1]), a outra metade compara-o com o id da fase (ph.id === currentPhase) — no stepper, na barra de fases e no painel de tempos — e o phaseLog grava o ordinal num campo chamado phaseId, consultado depois por timings[ph.id].

As duas leituras só coincidem enquanto o id de uma fase for a sua posição. Por isso o id que vem no JSON do workflow é descartado e reatribuído a partir do índice, tanto no buildPhasesFromData como no serializeWorkflow. Um workflow importado com ids fora de 1..N fazia o percurso avançar correctamente sem que nenhuma fase aparecesse como actual ou concluída. O tools/build.mjs recusa-se a construir se qualquer das duas funções voltar a aceitar o id do ficheiro.

Percursos activos (roles)

Um processo pode ter só o percurso clínico ou só o de farmácia (roles.clinico / roles.farmacia). O tracking do percurso desactivado continua a existir no objecto, parado na fase 1. Qualquer contagem ou indicador tem por isso de verificar roles antes de olhar para o estado — caso contrário conta percursos que não existem: os totais 🏥/💊, a barra de distribuição por fase e o throughput a 30 dias já o fazem.


Responsividade e acessibilidade

A app é utilizável em telemóvel a partir de 320 px de largura, sem scroll horizontal em nenhuma das três vistas. Abaixo de 900 px a barra de navegação passa a duas linhas e a variável --nav-h acompanha a mudança — qualquer elemento sticky deve usar top: var(--nav-h) em vez de um valor fixo.

As grelhas da app são definidas em estilos inline do React, que têm precedência sobre CSS normal. Por isso as media queries responsivas usam classes utilitárias com !important:

Classe Efeito
.rg-2.rg-6 colapsa N colunas para 2 e depois 1, por patamares
.rg-stats cartões de indicadores; mantém 2 colunas até 360 px
.rp-pad reduz o padding lateral dos contentores principais
.scroll-x põe conteúdo largo a rolar dentro do próprio contentor

Ao adicionar uma grelha nova, aplicar a classe correspondente — não basta o estilo inline.

Do lado da acessibilidade: as três vistas são um tablist navegável por setas/Home/End, os controlos de filtro expõem aria-pressed, e o foco por teclado é sempre visível (:focus-visible). São respeitados prefers-reduced-motion e prefers-contrast.

Todas as modais são diálogos a sério — as cinco imperativas (PIN, PDF, e-mail, templates, pré-visualização) e as cinco React (StepDrawer, StepDatesModal, NewProcessModal, ExportModal, ServiceStepsModal). Todas têm role="dialog" e aria-modal, fecham com Escape, confinam o Tab e devolvem o foco ao elemento que as abriu. As imperativas são tratadas pelo modalAccessibility() do bloco <script>; as React pelo hook useModalA11y, que além disso as renderiza num portal para o <body> — sem isso, marcar #view-tracking como inert tornaria inerte também a modal, que é renderizada lá dentro. Uma modal nova tem de usar um dos dois; não há terceiro caminho.

Não usar alert() nem confirm(). São bloqueantes, não são estilizáveis e trazem, em vários browsers, a opção de «impedir que esta página crie mais diálogos» — uma vez marcada, o confirm() devolve false sem perguntar nada e as operações passam a desaparecer sem explicação. Em vez deles há duas funções, definidas no bloco <script> e disponíveis também no bloco JSX:

Função Para quê
await askConfirm(mensagem, { titulo, confirmar, cancelar, tom }) pergunta de sim/não; devolve uma promessa, portanto o chamador é async
notify(mensagem, tom) aviso sem resposta, em cartão no canto inferior direito

O tom é 'info' | 'ok' | 'aviso' | 'erro'. A interface é desenhada pelo DialogHost, montado uma vez pelo App, que se regista em window.__SUPERMERCADOS_confirm e window.__SUPERMERCADOS_notify — é assim que o editor de workflow, que é código imperativo, usa os mesmos diálogos. Nas perguntas o foco arranca no Cancelar: ao contrário do confirm() nativo, um Enter dado à pressa não elimina nada.

About

Workflow e tracking de processos dos supermercados

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages