# Guia dos encontros e interesses CADÊNCIA

## Modelo local

A aplicação estática funciona sem servidor de dados. `cadencia:demo-bookings:v1` armazena avaliações e visitas; `cadencia:demo-interests:v1` armazena interesses. São dados exclusivos da origem e do navegador. Use apenas nome e telefone de teste, com aceite explícito. Não são coletados dados de saúde, CPF, e-mail ou notas livres.

O catálogo de `content/site.json` é autoritativo. Avaliação individual: 60 minutos, Nina ou Theo. Visita individual: 30 minutos, somente Theo. Datas abrangem hoje e os vinte dias seguintes, em `America/Sao_Paulo`, excluindo horários já passados. `weeklySlots` usa segunda=1 e domingo=7. Nenhum feriado é excluído sem regra explícita. Sobreposição compara profissional, dia e intervalos; intervalos adjacentes são permitidos. Interesse não participa da capacidade.

Web Locks coordena reservas e cancelamentos entre abas. Falta ou recusa de locks, corrupção e recusa de armazenamento resultam em erro, nunca em sucesso aparente. `/demo/` mostra encontros, cancelados e interesses; cancelar libera o intervalo e apagar dados remove somente as duas chaves CADÊNCIA. O modo local não controla disponibilidade entre dispositivos.

## Configurar Google Apps Script

1. Crie um projeto Apps Script e copie `gas/Code.gs`. Execute `npm run package` antes de copiar para incorporar o catálogo atual.
2. Configure `SPREADSHEET_ID` nas propriedades do script, ou execute `setup()` para criar uma planilha. `initialize()` cria as abas e valida os cabeçalhos sem sobrescrever dados incompatíveis.
3. `initialize()` gera `OPS_TOKEN` e `CANCEL_SECRET`. Mantenha ambos nas propriedades privadas do script. Use uma implantação demonstrativa autorizada, defina quem pode acessá-la e a política de retenção/exclusão.
4. Publique uma implantação Web App com o acesso apropriado e obtenha a URL terminada em `/exec`. Configure-a em `PUBLIC_DEMO_GAS_ENDPOINT`. Ative `PUBLIC_DEMO_BOOKING_MODE=demo_gas` para encontros e/ou `PUBLIC_DEMO_INTEREST_MODE=demo_gas` para interesses. Refaça o build.
5. O formulário informa o destino antes do envio e solicita o token privado em campo de senha. O token permanece apenas em RAM, não em URL, armazenamento, ZIP ou bundle. Um serviço público real deve usar um intermediário protegido com autorização por usuário; não distribuir o token operacional aos visitantes.

Os formulários são localmente independentes: a URL sozinha não muda o modo e habilitar encontros remotos não habilita interesses remotos. Não há alegação de planilha ativa sem validação de uma implantação real.

## Abas e colunas exatas

Primeira linha da aba **agenda**, nesta ordem:

```text
Id | RequestId | Fingerprint | ServiceId | ProfessionalId | Date | Time | DurationMinutes | Name | Phone | Status | CreatedAt | CancelHash
```

Primeira linha da aba **interesses**, nesta ordem:

```text
Id | RequestId | Fingerprint | ServiceId | Name | Phone | Status | CreatedAt | CancelHash
```

Datas, horas, nomes e telefones são tratados como texto. Nomes iniciados por sinais de fórmula são escapados. `Status` é `registered`, `cancelled` para encontros cancelados, ou `deleted` para registros excluídos. `CreatedAt` usa ISO UTC. `CancelHash` contém o hash da capacidade de cancelamento, não o token em claro.

## API e autorização

GET público aceita somente `action=availability`, `serviceId`, `professionalId` e `date`; retorna `{ok:true,slots:[...]}` sem PII ou códigos de registros. Os horários são recalculados pelo servidor a partir do catálogo e intervalos gravados.

POST usa JSON com `Content-Type: text/plain;charset=UTF-8`, permitindo a requisição simples do navegador ao Web App. Respostas devem ser legíveis via CORS; resposta opaca ou falha de CORS não demonstra sucesso. Não use `no-cors` como confirmação.

Gravação privada de encontro:

```json
{"action":"register_booking","opsToken":"TOKEN_PRIVADO","schemaVersion":"1.1.0","brandId":"cadencia-movimento-demo","requestId":"UUID_DA_TENTATIVA","kind":"booking","serviceId":"avaliacao-fisioterapia","professionalId":"theo-martins","date":"YYYY-MM-DD","time":"HH:mm","name":"Pessoa Teste","phone":"11999990000","demoAcknowledged":true}
```

Gravação privada de interesse usa `action=register_interest`, `kind=interest`, `serviceId=interesse-pilates` e os mesmos campos comuns, sem `professionalId`, `date` ou `time`. Campos excedentes são rejeitados. Nome é normalizado por trim e tem 2–80 caracteres. Telefone permite entrada de até 20 caracteres e é normalizado para 10 ou 11 dígitos.

`ScriptLock` protege a validação de intervalos e a gravação. Idempotência usa marca, tipo e `requestId`, com fingerprint dos campos normalizados. O mesmo ID e payload retorna o registro original; payload diferente retorna `nonce_mismatch`. O catálogo embarcado controla schema, elegibilidade, duração e horizonte, sem confiar nesses valores enviados pelo cliente.

Outras operações POST:

| Ação | Campos além de action | Autorização |
|---|---|---|
| `request_status` | `opsToken`, `kind`, `brandId`, `requestId` | Token operacional |
| `list_records` | `opsToken` | Token operacional |
| `delete_record` | `opsToken`, `kind`, `recordId` | Token operacional |
| `cancel_record` | `kind=booking`, `recordId`, `cancelToken` | Capacidade imprevisível específica do registro |

Cancelamento compara o hash da capacidade, cujo HMAC inclui marca, tipo, ID e nonce. ID isolado não autoriza a operação. Listagem e exclusão não têm rota pública. Exclusão limpa nome e telefone e marca o registro como `deleted`, preservando o nonce e fingerprint. Esses registros são omitidos da listagem e deixam de ocupar capacidade. Retry de um ID excluído retorna `request_closed` e não cria outro registro. Políticas de retenção devem considerar esses metadados de idempotência.

## Respostas e tentativas ambíguas

Sucesso legível: `{ok:true,status:"registered",recordId,cancelToken}`. Conflito: `{ok:false,code:"slot_conflict"}`. Validação e autorização rejeitadas são respostas definitivas. Timeout, erro de rede, resposta inválida, opaca, HTTP de erro ou `unavailable` podem deixar resultado desconhecido.

Uma tentativa ambígua mantém o mesmo nonce e payload, bloqueia novos envios, edição e fallback local. O usuário pode retomar o mesmo envio idempotente ou consultar `request_status` com autorização. Somente `not_found` autoritativo libera uma nova tentativa; não receber resposta não é prova de rejeição. A tentativa e seus dados de teste ficam em `cadencia:demo-pending:booking:v1` ou `cadencia:demo-pending:interest:v1` antes do envio. Reload restaura o nonce e mantém os campos bloqueados; apenas o token privado precisa ser informado novamente, pois não é persistido. Excluir encontros e interesses locais não elimina uma tentativa remota pendente. Não use esta demonstração para dados reais ou operações desassistidas.

Apagar dados locais não apaga registros do destino remoto. A visão `/demo/` lista somente dados locais; operações remotas requerem acesso privado ao servidor. Antes de habilitar uso real, validar identidade profissional, infraestrutura, autenticação por usuário, capacidade entre dispositivos, CORS, retenção e controles de acesso.
