# Runbook de desenvolvimento e operação ## Desenvolvimento local 1. Copiar `.env.example` para `.env`. 2. Executar `docker compose build`. 3. Gerar a chave com `docker compose run --rm app php artisan key:generate`. 4. Iniciar com `docker compose up -d`. 5. Preparar dados com `docker compose exec app php artisan migrate --seed`. 6. Abrir o portal em `http://localhost:8000` e emails em `http://localhost:8025`. ## Diagnóstico ```bash docker compose ps docker compose logs --tail=100 app docker compose logs --tail=100 queue docker compose exec app php artisan about docker compose exec app php artisan migrate:status docker compose exec postgres pg_isready -U reserva -d reserva docker compose exec redis redis-cli ping curl --fail http://localhost:8000/health/live curl --fail http://localhost:8000/health/ready ``` `/health/live` confirma apenas resposta HTTP. `/health/ready` verifica PostgreSQL e cache e deve ser usado pelo balanceador. Ambos omitem detalhes de serviços. Se readiness falhar, consultar os logs da aplicação e testar PostgreSQL/Redis separadamente com os comandos anteriores. ### Página em branco no desenvolvimento Se o HTML responder mas a interface React não aparecer, confirmar que `public/hot` contém `http://localhost:5173` e não `http://0.0.0.0:5173`. Depois validar os módulos: ```bash curl -I -H 'Origin: http://localhost:8000' http://localhost:5173/@vite/client curl -I -H 'Origin: http://localhost:8000' http://localhost:5173/resources/js/app.tsx ``` Para executar a validação backend use `docker compose exec app composer test`, não `php artisan test` diretamente. O script isola as variáveis de teste das variáveis `local` injetadas pelo Docker Compose e evita que jobs únicos deixem locks no Redis de desenvolvimento. Ambos devem responder com estado 200 e `Access-Control-Allow-Origin: http://localhost:8000`. Reiniciar o serviço com `docker compose restart node` se o ficheiro `public/hot` estiver desatualizado. Confirmar também que o HTML inicial já contém conteúdo SSR: ```bash curl -s http://localhost:8000/login | grep 'data-server-rendered="true"' ``` Se o endpoint Vite devolver um erro SSR, procurar primeiro componentes globais montados em `withApp` que chamem hooks Inertia como `usePage()`. Os elementos irmãos do argumento `app` não pertencem ao respetivo contexto; devem receber as propriedades através do segundo argumento de `withApp`. ### Login regressa ao mesmo ecrã Um `POST /login` com resposta 302 seguido novamente de `/login` indica normalmente credenciais recusadas, não indisponibilidade do portal. Em desenvolvimento, confirmar primeiro se a base tem dados: ```bash docker compose exec app php artisan tinker --execute="dump(App\\Models\\User::count());" ``` Se o resultado for zero e esta for a base local descartável, repor apenas os dados fictícios com: ```bash docker compose exec app php artisan db:seed --force ``` As contas são `demo@escola.pt` e `admin@escola.pt`, ambas com a palavra-passe de desenvolvimento `password`. Nunca executar o seeder para corrigir acessos em produção; aí deve ser usado o processo administrativo de contas/recuperação. ## Atualização de código ```bash docker compose exec app composer install docker compose exec node npm install docker compose exec app php artisan migrate --force docker compose exec app php artisan optimize:clear docker compose restart queue scheduler ``` ## Integração Snipe-IT > **Instância de produção:** é proibido criar, editar ou eliminar produtos, ativos ou quaisquer dados mestres. Diagnósticos e testes técnicos fazem apenas pedidos `GET`. Checkout e check-in só podem ocorrer como movimentos operacionais legítimos iniciados por um operador no portal. Um ativo que já esteja atribuído não pode ser usado para testar ou repetir um checkout. Definir no ambiente, sem colocar tokens no repositório: ```dotenv SNIPE_IT_URL=https://inventario.exemplo.pt SNIPE_IT_READ_TOKEN= SNIPE_IT_OPERATIONAL_TOKEN= SNIPE_IT_TIMEOUT=10 SNIPE_IT_PAGE_SIZE=100 SNIPE_IT_RETRIES=3 SNIPE_IT_RETRY_DELAY_MS=250 ``` O token de leitura deve pertencer a uma conta técnica com acesso apenas de leitura aos ativos. O token operacional deve ser separado e ter apenas as permissões necessárias para checkout/check-in. A aplicação acrescenta `/api/v1` automaticamente quando a URL não o contém. Nunca validar o token operacional através de um checkout/check-in artificial. A validade de leitura pode ser confirmada pelo teste de ligação administrativo ou por uma sincronização. A permissão operacional é confirmada apenas no primeiro movimento institucional legítimo, sobre um ativo existente, atualmente não atribuído e previamente reservado no portal. Operações: ```bash # Sincronização imediata e resultado no terminal docker compose exec app php artisan snipe:sync-assets # Colocar na fila docker compose exec app php artisan snipe:sync-assets --queue # Confirmar o agendamento horário docker compose exec app php artisan schedule:list ``` Administradores podem testar a ligação, iniciar uma execução e consultar as últimas dez sincronizações em `http://localhost:8000/administracao/snipe-it`. Uma execução falhada mantém o último catálogo válido e não desativa registos ausentes. Consultar `sync_runs`, os logs do worker e o painel antes de repetir. O teste de ligação e `snipe:sync-assets` consultam a API e alteram apenas o cache local do portal. Não executar comandos improvisados com `POST`, `PUT`, `PATCH` ou `DELETE` contra a URL configurada. Nunca copiar para terminais partilhados, tickets ou documentação os tokens, a URL privada ou corpos integrais devolvidos pela API. ## Microsoft Entra ID Registar uma aplicação **single tenant** do tipo Web. O redirect URI tem de corresponder exatamente ao domínio do portal e ao caminho `/auth/microsoft/callback`. Em produção é obrigatório HTTPS. ```dotenv MICROSOFT_ENABLED=true MICROSOFT_CLIENT_ID= MICROSOFT_CLIENT_SECRET= MICROSOFT_TENANT_ID= MICROSOFT_REDIRECT_URI=https://reservas.exemplo.pt/auth/microsoft/callback MICROSOFT_TIMEOUT=10 MICROSOFT_METADATA_CACHE_SECONDS=3600 ``` Depois de aplicar a configuração e limpar a cache (`php artisan optimize:clear`): 1. entrar com a conta local; 2. abrir `Definições > Segurança` e confirmar novamente a palavra-passe; 3. escolher **Associar conta Microsoft** e concluir a autenticação institucional; 4. terminar sessão e validar **Entrar com a conta Microsoft** no login; 5. confirmar os eventos `identity.microsoft_linked` e `identity.microsoft_login` em auditoria. O portal não associa contas por email. Uma identidade não associada recebe uma instrução para entrar localmente e fazer a associação explícita. Remover a associação também exige confirmação recente da palavra-passe e cria `identity.microsoft_unlinked`. Nunca copiar códigos ou tokens para tickets/logs. ## Entregas, devoluções e reconciliação Utilizadores com perfil `inventory_operator` ou `administrator` acedem a `http://localhost:8000/operacoes`. A fila inclui as reservas do dia e todos os processos entregues, atrasados, devolvidos ou com incidente ainda pendente. O campo inicial aceita referência, nome, email, etiqueta, ID Snipe-IT ou a saída de um leitor QR/código de barras configurado como teclado. Procedimento de entrega: 1. preparar a requisição; 2. confirmar fisicamente todos os equipamentos e o estado à saída; 3. confirmar o ID do destinatário no Snipe-IT; 4. registar a entrega e verificar a mensagem de sucesso; 5. se existir erro, não voltar a submeter antes de consultar o estado do ativo no Snipe-IT. Antes do `POST` de checkout, o portal consulta o estado remoto atual. Se o ativo já estiver atribuído — ao destinatário esperado ou a qualquer outro — a entrega é bloqueada sem escrita no Snipe-IT. Não contornar este bloqueio nem alterar o ativo diretamente para prosseguir. Procedimento de devolução: 1. localizar a requisição ou ler a etiqueta; 2. verificar equipamento, acessórios, danos e localização; 3. registar a devolução; 4. criar um incidente e bloqueio temporal quando o ativo não deva voltar ao catálogo; 5. concluir o processo apenas depois de todos os itens estarem devolvidos. O check-in só é enviado se existir um checkout confirmado pelo portal para esse item e requisitante, e se a atribuição atual no Snipe-IT corresponder ao mesmo utilizador. Se alguma destas provas falhar, não efetuar o check-in manualmente a partir do portal; reconciliar o histórico com o responsável do inventário. Uma operação parcial fica em `incident`. Consultar `handover_records` para movimentos confirmados e os eventos `reservation.delivery_failed`/`reservation.return_failed` em `audit_logs`. Comparar cada item com o estado atual no Snipe-IT; corrigir apenas os itens ainda `ready` ou `delivered`. Não alterar estados diretamente na base de dados nem repetir em bloco, porque as escritas externas são deliberadamente não idempotentes e não têm retry automático. ## Ciclo de vida das pré-reservas As requisições pendentes têm, por defeito, um prazo de decisão de 48 horas. O scheduler coloca a expiração na fila a cada cinco minutos. Para operar ou diagnosticar manualmente: ```bash # Expirar imediatamente os pedidos vencidos docker compose exec app php artisan reservations:expire # Colocar a operação na fila docker compose exec app php artisan reservations:expire --queue # Confirmar ambos os agendamentos docker compose exec app php artisan schedule:list ``` Uma expiração ou cancelamento muda itens `held`, `reserved`, `ready` e `delivered` para `cancelled`, libertando o período. Confirmar os eventos `reservation.expired` ou `reservation.cancelled` em `audit_logs` quando existir uma divergência operacional. ## Aprovações Utilizadores com perfil `approver` ou `administrator` acedem a `http://localhost:8000/aprovacoes`. Uma decisão válida deve criar uma linha em `approval_actions` e outra em `audit_logs`. Se a aprovação indicar que a disponibilidade mudou, confirmar primeiro a atribuição/estado sincronizado do ativo e conflitos entretanto criados; não alterar o estado diretamente na base de dados. As decisões geram jobs de notificação. Em desenvolvimento, confirmar o email em `http://localhost:8025` e a notificação interna em `http://localhost:8000/notificacoes`. Se uma mensagem não chegar, consultar primeiro `docker compose logs --tail=100 queue` e a tabela `failed_jobs`; não repetir a decisão já confirmada. ## Administração e relatórios Administradores usam: - `http://localhost:8000/administracao/utilizadores` para contas, perfis e IDs Snipe-IT; - `http://localhost:8000/administracao/regras` para políticas aplicadas a novos pedidos; - `http://localhost:8000/administracao/notificacoes/modelos` para assuntos e mensagens; - `http://localhost:8000/administracao/snipe-it` para diagnóstico da integração. As alterações são registadas em `audit_logs`. A margem de uma reserva já submetida não muda quando a política é alterada; confirmar `buffer_starts_at` e `buffer_ends_at` do item ao investigar um conflito. Administradores e auditores consultam `http://localhost:8000/relatorios` e `http://localhost:8000/auditoria`. O CSV usa `;`, UTF-8 com BOM e neutralização de fórmulas para abertura segura em aplicações de folha de cálculo. Cada pedido está limitado a 366 dias; para análises plurianuais, exportar períodos separados. Aprovadores, operadores, administradores e auditores consultam o planeamento consolidado em `http://localhost:8000/calendario-global`. ## Produção Antes de disponibilizar: - definir `APP_ENV=production`, `APP_DEBUG=false` e HTTPS; - substituir todas as palavras-passe do exemplo; - montar segredos fora do repositório; - remover exposição pública das portas PostgreSQL, Vite e Mailpit; - configurar volumes persistentes e backups cifrados; - executar `php artisan optimize` e frontend compilado; - definir health checks, alertas e rotação de logs; - testar recuperação de backup e falha Snipe-IT. ## Backups Salvaguardar diariamente PostgreSQL e uploads. Manter retenção semanal/mensal e uma cópia fora do servidor. Segredos são salvaguardados através de procedimento separado. Executar um teste de restauro periódico e registar o resultado. ```bash # Cria dump custom, valida-o e acrescenta checksum SHA-256 ops/backup-postgres.sh /caminho/cifrado/backups # Restaura numa base temporária isolada e elimina-a no fim ops/test-restore-postgres.sh /caminho/cifrado/backups/reserva-AAAAMMDDTHHMMSSZ.dump ``` Os scripts obtêm o nome da base e utilizador diretamente do contentor PostgreSQL, recusam identificadores inesperados e nunca restauram sobre a base principal. O destino tem de estar cifrado e deve ser replicado para fora do servidor. Monitorizar a execução e a idade do último ficheiro; um dump sem ensaio de restauro não conta como recuperação validada. Usar [docs/PRODUCTION_CHECKLIST.md](PRODUCTION_CHECKLIST.md) para registar responsáveis e evidências antes do piloto.