diff --git a/.env.example b/.env.example index 7ae8b3b..6927cd9 100644 --- a/.env.example +++ b/.env.example @@ -24,3 +24,9 @@ WORK_WINDOW_END=18:00 LIDARR_URL= LIDARR_API_KEY= WEBHOOK_SECRET= + +# Ntfy notifications (optional, leave disabled if not using). +NTFY_ENABLED=false +NTFY_URL= +NTFY_TOPIC= +NTFY_TOKEN= diff --git a/AGENTS.md b/AGENTS.md index d278c2c..b3d5a7e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,6 +38,7 @@ internal/ models/ # tipos compartilhados (Job, Log, Config) store/ # repositórios SQLite + migrations scanner/ # library scanner (navegação + scan recursivo) + ntfy/ # cliente de notificações ntfy web/ styles/ # input.css + bundles DaisyUI (fonte) @@ -77,6 +78,10 @@ Variáveis de ambiente (defaults). Sobrescritas por overrides no SQLite via `/se | `WORK_WINDOW_ENABLED` | `false` | Habilita janela de trabalho | | `WORK_WINDOW_START` | `08:00` | Início da janela (HH:MM) | | `WORK_WINDOW_END` | `18:00` | Fim da janela (HH:MM) | +| `NTFY_ENABLED` | `false` | Habilita notificações ntfy | +| `NTFY_URL` | — | URL base do servidor ntfy | +| `NTFY_TOPIC` | — | Tópico para publicar notificações | +| `NTFY_TOKEN` | — | Token de autenticação (opcional) | ## Componentes Principais @@ -104,6 +109,11 @@ Gate que libera ou retém tarefas conforme a janela de trabalho. - Scan recursivo: detecta FLACs, agrupa por diretório (álbum), retorna lista. - Endpoints REST usados pela página `/library`. +### Ntfy (`internal/ntfy/`) +- Cliente de notificações para servidor ntfy (self-hosted ou público). +- Envia notificação de sucesso/falha ao final de cada job. +- Respeita `DRY_RUN`; token opcional via `Authorization: Bearer`. + ### Web UI (`internal/web/`) Páginas: - `/` — dashboard: status, fila, workers, janela de trabalho diff --git a/README.md b/README.md index 41ab237..039e272 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Em ambos os fluxos, o Coda converte os arquivos, valida o resultado, remove os F - Interface web: dashboard, tarefas, logs, navegação/scan de FLACs e configurações. - Persistência em **SQLite** (jobs, logs e configurações). - Modo **dry run** para simular sem converter nem apagar. +- Notificações via **ntfy** (self-hosted ou público) ao final de cada job. ## Stack @@ -100,14 +101,18 @@ Definida por variáveis de ambiente (defaults) e sobrescrita pela interface web | `WORK_WINDOW_ENABLED` | `false` | Habilita a janela de trabalho | | `WORK_WINDOW_START` | `08:00` | Início da janela (HH:MM) | | `WORK_WINDOW_END` | `18:00` | Fim da janela (HH:MM) | +| `NTFY_ENABLED` | `false` | Habilita notificações ntfy | +| `NTFY_URL` | — | URL base do servidor ntfy | +| `NTFY_TOPIC` | — | Tópico para publicar notificações | +| `NTFY_TOKEN` | — | Token de autenticação (opcional) | ## Estrutura do Projeto ``` cmd/server/ # entrypoint internal/ # config, scheduler, webhook, queue, worker, - # transcoder, validator, lidarr, logger, web, - # models, store, scanner + # transcoder, validator, lidarr, logger, web, + # models, store, scanner, ntfy web/ # styles/ (fonte CSS), static/ (gerado), templates/ data/ # volume local do SQLite ``` diff --git a/docs/architecture.md b/docs/architecture.md index aac6e2b..4d8bd46 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -166,6 +166,14 @@ Responsável por: - exposição dos resultados via API REST para a interface web; - enfileiramento dos álbuns selecionados para conversão. +### Ntfy Client + +Cliente HTTP responsável por: + +- envio de notificações de sucesso/falha ao final de cada job; +- suporte a servidor self-hosted ou público; +- autenticação opcional via token (`Authorization: Bearer`). + ### Web UI Interface para operação e monitoramento do serviço. @@ -203,6 +211,7 @@ internal/ models/ store/ scanner/ + ntfy/ web/ styles/ diff --git a/docs/blueprints/10-ntfy-notifications.md b/docs/blueprints/10-ntfy-notifications.md new file mode 100644 index 0000000..91e659d --- /dev/null +++ b/docs/blueprints/10-ntfy-notifications.md @@ -0,0 +1,116 @@ +# Blueprint 10 — Ntfy Notifications + +- **ID:** BP-10 +- **Feature:** Notificações via ntfy sobre sucesso/falha de jobs +- **Requisitos:** RF-017 +- **Depende de:** BP-05 (Worker Pool) +- **Status:** pendente + +## Objetivo + +Implementar um cliente HTTP que envia notificações para um servidor [ntfy](https://ntfy.sh) (self-hosted ou público) ao final de cada job, informando se a conversão foi concluída com sucesso ou falhou. Respeitar `DRY_RUN`. + +## Contexto + +- BP-05 definiu o worker pool com um fluxo de `process` que termina chamando `finishJob` (status `done` ou `error`). O notificador será chamado antes do finish. +- `config.Service` fornecerá `NtfyEnabled`, `NtfyURL`, `NtfyTopic`, `NtfyToken`. +- O padrão segue a mesma abordagem do `Rescanner`: interface injetada no `worker.Pool`, substituível para testes. + +## Restrições e Convenções + +- Sem comentários; erros com `%w`; logs com `slog.With("job_id", id)`. +- **Resiliência:** falha ao enviar notificação **não** deve interromper o pipeline. Logar o erro e seguir. +- `DRY_RUN=true`: **não** enviar notificação; logar a intenção. +- Se `NTFY_ENABLED=false` ou `NtfyURL`/`NtfyTopic` vazios: pular (log debug). +- Token (`NTFY_TOKEN`) é opcional; se presente, enviar como `Authorization: Bearer `. +- Timeout de 10s no HTTP client. +- Mensagens em inglês. + +## Design + +### `internal/ntfy/client.go` + +```go +type Client struct { + cfg *config.Service + hc *http.Client +} + +func New(cfg *config.Service) *Client + +// Notify envia uma notificação com o resultado do job. +// Implementa worker.Notifier. +func (c *Client) Notify(ctx context.Context, job *models.Job) error +``` + +- Endpoint: `POST {NtfyURL}/{NtfyTopic}` +- Prioridade: `high` para erro, `default` para sucesso. +- Tags: `white_check_mark` (done), `x` (error). +- Corpo: `json.Marshal(ntfyMessage{...})` com title, message, priority, tags. +- Header `Authorization: Bearer ` apenas se token != "". + +### `internal/worker/notifier.go` + +```go +type Notifier interface { + Notify(ctx context.Context, job *models.Job) error +} + +type NoopNotifier struct{} + +func (NoopNotifier) Notify(ctx context.Context, job *models.Job) error { + return nil +} +``` + +Adicionar campo `Notifier` ao `worker.Pool` e chamar `p.Notifier.Notify(ctx, job)` antes de `finishJob`. + +### Wiring em `cmd/server/main.go` + +- `notifier := ntfy.New(cfgSvc)` (ou `NoopNotifier{}` se `NtfyEnabled=false`). +- Passar `notifier` ao `worker.New(..., notifier)`. + +### Config (novas chaves editáveis) + +| Key | Default | Descrição | +|-----|---------|-----------| +| `NTFY_ENABLED` | `false` | Habilita notificações ntfy | +| `NTFY_URL` | — | URL base do servidor ntfy | +| `NTFY_TOPIC` | — | Tópico para publicar | +| `NTFY_TOKEN` | — | Token de autenticação (opcional) | + +## Tarefas + +- [ ] Adicionar `Notifier` interface em `internal/worker/`. +- [ ] Criar `internal/ntfy/client.go` (`New`, `Notify`). +- [ ] Adicionar keys ntfy ao `config` (Key + validação + editableKeys + Config struct + buildConfig). +- [ ] Chamar `Notifier.Notify` no `worker.process` antes de `finishJob`. +- [ ] Wiring no `main.go`: `ntfy.New(cfgSvc)` → `worker.New(...)`. +- [ ] Atualizar documentos: `README.md`, `AGENTS.md`, `requirements.md`, `architecture.md`. + +## Testes + +`internal/ntfy/client_test.go` (httptest.Server): + +- [ ] `Notify` envia `POST /` com prioridade/título/tags corretos para job concluído. +- [ ] `Notify` envia prioridade `high` e tag `x` para job com erro. +- [ ] `DRY_RUN=true` → nenhum request é feito. +- [ ] `NtfyEnabled=false` → nenhum request, retorna nil. +- [ ] Token presente → header `Authorization: Bearer `. +- [ ] Timeout/erro de rede → erro logado, não interrompe. + +`internal/worker/worker_test.go` (atualizar): + +- [ ] Verificar que `Notifier` fake é chamado antes do `finishJob` tanto em sucesso quanto em erro. +- [ ] Verificar que `NoopNotifier` não causa efeitos colaterais. + +## Definição de Pronto + +- DoD comum atendido. +- `go test ./internal/ntfy/... ./internal/worker/...` verde. +- `make run-prod`: notificações enviadas ao final de cada job (se habilitado). + +## Arquivos + +- **Criar:** `internal/ntfy/client.go` (+ `_test.go`), `internal/worker/notifier.go`. +- **Modificar:** `internal/config/config.go`, `internal/worker/worker.go`, `cmd/server/main.go`. diff --git a/docs/blueprints/README.md b/docs/blueprints/README.md index 7c2d465..4e04e52 100644 --- a/docs/blueprints/README.md +++ b/docs/blueprints/README.md @@ -33,6 +33,7 @@ O que **falta** está coberto pelos blueprints abaixo. | 07 | [Webhook Trigger](07-webhook-trigger.md) ~~~✅~~~ | Endpoint de webhook | RF-001, RF-012 | 05, 06 | | 08 | [Library Scan & Convert](08-library-scan-and-convert.md) ~~~✅~~~ | Scan/navegação + conversão manual | RF-015 | 05 | | 09 | [Web Interface](09-web-interface.md) ~~~✅~~~ | UI completa | RF-009, RF-010, RF-016 | 02, 03, 05, 08 | +| 10 | [Ntfy Notifications](10-ntfy-notifications.md) | Notificações via ntfy | RF-017 | 05 | Recomenda-se executá-los em ordem numérica. Cada blueprint assume que os anteriores dos quais depende já foram concluídos e integrados. diff --git a/docs/requirements.md b/docs/requirements.md index dcf30b6..3fcc271 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -99,7 +99,8 @@ A aplicação deverá registrar: - duração; - arquivos convertidos; - erros; -- chamadas ao Lidarr. +- chamadas ao Lidarr; +- notificações enviadas. Os logs deverão ser persistidos em SQLite e permanecer disponíveis para consulta via interface web. @@ -190,6 +191,10 @@ TIMEZONE=America/Sao_Paulo WORK_WINDOW_ENABLED=false WORK_WINDOW_START=08:00 WORK_WINDOW_END=18:00 +NTFY_ENABLED=false +NTFY_URL= +NTFY_TOPIC= +NTFY_TOKEN= ``` --- @@ -256,6 +261,19 @@ Alterações têm efeito imediato (não exigem reinicialização do container). --- +### RF-017 Notificações via ntfy + +A aplicação deverá enviar notificações para um servidor [ntfy](https://ntfy.sh) (self-hosted ou público) ao final de cada job, informando o resultado da conversão. + +- Notificação de **sucesso**: prioridade `default`, tag `white_check_mark`, título com artista/álbum. +- Notificação de **falha**: prioridade `high`, tag `x`, título com artista/álbum e mensagem de erro. +- Falha ao enviar notificação **não** interrompe o pipeline — o job não é revertido. +- Respeita o modo `DRY_RUN`: nenhuma notificação é enviada. +- Desabilitada por padrão (`NTFY_ENABLED=false`). +- Suporte a token de autenticação (`Authorization: Bearer`). + +--- + ## 2. Requisitos Não Funcionais ### RNF-001