ML-165: implementation

This commit is contained in:
Claudio Ortolina
2026-05-05 13:43:18 +01:00
parent 008dc20ff4
commit cccd28a9f9
10 changed files with 1084 additions and 86 deletions
@@ -1,16 +1,25 @@
---
id: ML-165
title: Mute and resolve production errors from pi
status: To Do
status: Done
assignee: []
created_date: '2026-05-05 11:20'
updated_date: '2026-05-05 12:10'
created_date: "2026-05-05 11:20"
updated_date: "2026-05-05 12:27"
labels: []
dependencies: []
references:
- >-
doc-12 -
Research-Mute-and-Resolve-Production-Errors-Implementation-Routes.md
modified_files:
- lib/music_library/errors.ex
- lib/music_library_web/controllers/error_controller.ex
- lib/music_library_web/controllers/error_json.ex
- lib/music_library_web/router.ex
- test/music_library/errors_test.exs
- test/music_library_web/controllers/error_controller_test.exs
- .pi/extensions/prod-errors/index.ts
- docs/architecture.md
priority: medium
ordinal: 10000
---
@@ -18,35 +27,40 @@ ordinal: 10000
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Extend the production error tooling in pi so that it's possible to:
1. Mute and resolve issues from the `prod-errors` TUI (user action)
2. Mute and resolve issues via a tool (pi action)
There are no endpoints for these two actions, so we would need to extend the application's v1/api/ endpoints to support them.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 POST /api/v1/errors/:id/mute sets muted=true and returns 200 with updated error
- [ ] #2 POST /api/v1/errors/:id/unmute sets muted=false and returns 200 with updated error
- [ ] #3 POST /api/v1/errors/:id/resolve sets status=:resolved and returns 200 with updated error
- [ ] #4 POST /api/v1/errors/:id/unresolve sets status=:unresolved and returns 200 with updated error
- [ ] #5 All four POST endpoints return 401 without Bearer token
- [ ] #6 All four POST endpoints return 404 for non-existent error ID
- [ ] #7 All four POST endpoints return 404 for non-integer ID
- [ ] #8 pi tools (mute_production_error, unmute_production_error, resolve_production_error, unresolve_production_error) work correctly
- [ ] #9 /prod-errors TUI: M key toggles mute state on selected error with visual feedback
- [ ] #10 /prod-errors TUI: R key toggles resolve/unresolve status on selected error with visual feedback
- [ ] #11 /prod-errors TUI help text shows new M and R keybindings
- [ ] #12 All context function tests pass
- [ ] #13 All controller tests pass
- [ ] #14 Documentation updated: docs/architecture.md reflects new endpoints and context description
- [x] #1 POST /api/v1/errors/:id/mute sets muted=true and returns 200 with updated error
- [x] #2 POST /api/v1/errors/:id/unmute sets muted=false and returns 200 with updated error
- [x] #3 POST /api/v1/errors/:id/resolve sets status=:resolved and returns 200 with updated error
- [x] #4 POST /api/v1/errors/:id/unresolve sets status=:unresolved and returns 200 with updated error
- [x] #5 All four POST endpoints return 401 without Bearer token
- [x] #6 All four POST endpoints return 404 for non-existent error ID
- [x] #7 All four POST endpoints return 404 for non-integer ID
- [x] #8 pi tools (mute_production_error, unmute_production_error, resolve_production_error, unresolve_production_error) work correctly
- [x] #9 /prod-errors TUI: M key toggles mute state on selected error with visual feedback
- [x] #10 /prod-errors TUI: R key toggles resolve/unresolve status on selected error with visual feedback
- [x] #11 /prod-errors TUI help text shows new M and R keybindings
- [x] #12 All context function tests pass
- [x] #13 All controller tests pass
- [x] #14 Documentation updated: docs/architecture.md reflects new endpoints and context description
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
## Implementation Plan: Route A — Four dedicated POST endpoints
### Overview
@@ -158,6 +172,7 @@ post "/errors/:id/unresolve", ErrorController, :unresolve
**File:** `test/music_library/errors_test.exs`
Add a `describe "mute_error/1, unmute_error/1, resolve_error/1, unresolve_error/1"` block with tests for:
- `mute_error/1` sets `muted` to `true`
- `unmute_error/1` sets `muted` to `false`
- `resolve_error/1` sets `status` to `:resolved`
@@ -168,6 +183,7 @@ Add a `describe "mute_error/1, unmute_error/1, resolve_error/1, unresolve_error/
**File:** `test/music_library_web/controllers/error_controller_test.exs`
Add a `describe "POST /api/v1/errors/:id/mute|unmute|resolve|unresolve"` block with tests for:
- Each endpoint returns 401 without Bearer token
- Each endpoint returns 200 with updated error on success
- Each endpoint returns 404 for non-existent ID
@@ -187,6 +203,7 @@ Register four new tools after the existing `fetch_production_error` tool:
4. **`unresolve_production_error`** — POSTs to `/api/v1/errors/:id/unresolve`
Each tool:
- Takes a single `id` (number) parameter
- Validates env vars (`PI_API_TOKEN`, `PI_SERVICE_FQDN_WEB`) like existing tools
- Makes a POST request with Bearer auth
@@ -196,6 +213,7 @@ Each tool:
Add a shared helper `postApi<T>` for POST requests (similar to the existing `fetchApi<T>` for GET, adding `method: "POST"`).
Add prompt guidelines:
- `mute_production_error`: "Use mute_production_error to silence notifications for a noisy or already-addressed production error."
- `unmute_production_error`: "Use unmute_production_error to re-enable notifications for a previously muted error."
- `resolve_production_error`: "Use resolve_production_error to mark a production error as resolved when the underlying issue has been fixed."
@@ -224,6 +242,7 @@ Add to the `ErrorBrowser` class:
3. **Update help text** in `renderList` and `renderDetail` to show the new keys.
Keybinding logic (list mode):
```
M → if error.muted → POST /unmute; else → POST /mute
R → if error.status === "resolved" → POST /unresolve; else → POST /resolve
@@ -232,6 +251,7 @@ R → if error.status === "resolved" → POST /unresolve; else → POST /resolve
After a successful API call, update the local `ErrorListItem` in `this.errors` and call `this.invalidate()`.
**Visual feedback on success:**
- The error's list entry re-renders immediately: the `[MUTED]` label appears/disappears, and the status badge (`[RESOLVED]` / `[UNRESOLVED]`) updates.
- In detail mode, the header line (`Status: … | Muted: …`) updates on next render.
- On failure: a toast notification via `this.notify()` displays the error message.
@@ -245,6 +265,7 @@ After a successful API call, update the local `ErrorListItem` in `this.errors` a
Update the `@moduledoc` to reflect that the module now handles both queries and mutations. The current text describes it as read-only — add a line noting that it also provides `mute_error/1`, `unmute_error/1`, `resolve_error/1`, and `unresolve_error/1` for mutating error state.
**File:** `docs/architecture.md`
- Under the Routes section (if it enumerates API routes), add the four new POST endpoints: `/api/v1/errors/:id/mute`, `/api/v1/errors/:id/unmute`, `/api/v1/errors/:id/resolve`, `/api/v1/errors/:id/unresolve`.
- Under Contexts → `Errors`, update the description from "Read-only queries" to "Queries and mutations for production error data". Note that muting an error suppresses future email notifications via `ErrorTracker.ErrorNotifier` (which checks the `muted` field before dispatching).
- Under the Controller table, update the `ErrorController` row to include the four new action routes.
@@ -253,16 +274,16 @@ Update the `@moduledoc` to reflect that the module now handles both queries and
## Architecture Impact Summary
| Component | Change |
|---|---|
| `MusicLibrary.Errors` | +4 public functions, +1 private helper, updated moduledoc |
| `MusicLibraryWeb.ErrorController` | +4 actions, +1 private helper |
| `MusicLibraryWeb.ErrorJSON` | +1 render function |
| `MusicLibraryWeb.Router` | +4 POST routes |
| `.pi/extensions/prod-errors/index.ts` | +4 tools, +2 TUI methods, +2 keybindings, updated help text |
| `test/music_library/errors_test.exs` | +~8 tests (including idempotency for all four actions) |
| `test/music_library_web/controllers/error_controller_test.exs` | +~12 tests |
| `docs/architecture.md` | Update errors context description, add routes, update controller table |
| Component | Change |
| -------------------------------------------------------------- | ---------------------------------------------------------------------- |
| `MusicLibrary.Errors` | +4 public functions, +1 private helper, updated moduledoc |
| `MusicLibraryWeb.ErrorController` | +4 actions, +1 private helper |
| `MusicLibraryWeb.ErrorJSON` | +1 render function |
| `MusicLibraryWeb.Router` | +4 POST routes |
| `.pi/extensions/prod-errors/index.ts` | +4 tools, +2 TUI methods, +2 keybindings, updated help text |
| `test/music_library/errors_test.exs` | +~8 tests (including idempotency for all four actions) |
| `test/music_library_web/controllers/error_controller_test.exs` | +~12 tests |
| `docs/architecture.md` | Update errors context description, add routes, update controller table |
**No changes:** supervision tree, PubSub, schemas, migrations, Oban workers, LiveViews, external APIs, production infrastructure.
@@ -282,4 +303,44 @@ Zero incremental cost. No external API calls.
## Production Infrastructure
No changes needed (no new env vars, no DNS, no firewall, no special migration handling).
<!-- SECTION:PLAN:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
## Summary
Added mute/unmute/resolve/unresolve capability for production errors across the full stack:
### Backend (Elixir/Phoenix)
- **`MusicLibrary.Errors`**: Added 4 public functions (`mute_error/1`, `unmute_error/1`, `resolve_error/1`, `unresolve_error/1`) that delegate to `ErrorTracker`'s built-in mutation functions. `resolve/1` and `unresolve/1` handle the idempotent case (already-resolved/already-unresolved) gracefully.
- **`MusicLibraryWeb.ErrorController`**: Added 4 POST actions (`mute/2`, `unmute/2`, `resolve/2`, `unresolve/2`) with a shared `perform_action/3` helper that parses integer IDs, delegates to context, and returns proper JSON responses (200, 404, 422).
- **`MusicLibraryWeb.ErrorJSON`**: Added `update/1` render function for the `:update` template atom.
- **`MusicLibraryWeb.Router`**: Added 4 POST routes under the authenticated `/api/v1` scope.
### Tests
- **Context tests** (9 new): Test all four functions on success, not_found, and idempotency for each.
- **Controller tests** (6 auth + 6 functional): Test 401 without token, 200 with updated state, 404 for non-existent/non-integer IDs.
### Pi Extension (TypeScript)
- **`postApi<T>` helper**: Shared POST request helper with Bearer auth and validation.
- **4 new tools**: `mute_production_error`, `unmute_production_error`, `resolve_production_error`, `unresolve_production_error` — each takes an error ID, POSTs to the corresponding endpoint, and returns success/error.
- **TUI keybindings**: `M` (Shift+M) toggles mute, `R` (Shift+R) toggles resolve/unresolve on the selected error in both list and detail modes. Local state updates immediately on success with toast notifications.
- **Help text**: Updated in both list and detail modes to show the new keys.
### Documentation
- `docs/architecture.md`: Updated Errors context description from "Read-only" to "Queries and mutations", added new POST routes to ErrorController table.
- `lib/music_library/errors.ex`: Updated `@moduledoc` to reflect mutation capabilities.
### Design decisions
- Used `ErrorTracker.mute/1`, `unmute/1`, `resolve/1`, `unresolve/1` (which emit telemetry events) rather than raw `Ecto.Changeset.change/2`
- `resolve_error/1` and `unresolve_error/1` handle the already-resolved/unresolved case explicitly (ErrorTracker's functions pattern-match on current state and would crash otherwise)
- POST endpoints return the updated error as JSON (consistent with GET responses), using the same `error/1` render helper
<!-- SECTION:FINAL_SUMMARY:END -->
@@ -1,9 +1,10 @@
---
id: doc-12
title: 'Research: Mute and Resolve Production Errors Implementation Routes'
title: "Research: Mute and Resolve Production Errors Implementation Routes"
type: specification
created_date: '2026-05-05 11:24'
created_date: "2026-05-05 11:24"
---
# Research: Mute and Resolve Production Errors Implementation Routes
**Task:** ML-165 — Mute and resolve production errors from pi
@@ -13,21 +14,21 @@ created_date: '2026-05-05 11:24'
### Backend (Elixir/Phoenix)
| Component | File | Status |
|---|---|---|
| Error schema | `deps/error_tracker/lib/error_tracker/schemas/error.ex` | Has `status` (:resolved/:unresolved) and `muted` (boolean) fields |
| Error context | `lib/music_library/errors.ex` | Read-only: `list_errors/1`, `get_error/1` — no update functions |
| Error controller | `lib/music_library_web/controllers/error_controller.ex` | `GET /api/v1/errors` (index), `GET /api/v1/errors/:id` (show) |
| Error JSON views | `lib/music_library_web/controllers/error_json.ex` | Renders error/occurrence data |
| Router | `lib/music_library_web/router.ex` | `/api/v1/errors` routes in API scope (Bearer token auth) |
| Auth | `lib/music_library_web/auth.ex` | `require_api_token/2` plug — Bearer token comparison |
| Component | File | Status |
| ---------------- | ------------------------------------------------------- | ----------------------------------------------------------------- |
| Error schema | `deps/error_tracker/lib/error_tracker/schemas/error.ex` | Has `status` (:resolved/:unresolved) and `muted` (boolean) fields |
| Error context | `lib/music_library/errors.ex` | Read-only: `list_errors/1`, `get_error/1` — no update functions |
| Error controller | `lib/music_library_web/controllers/error_controller.ex` | `GET /api/v1/errors` (index), `GET /api/v1/errors/:id` (show) |
| Error JSON views | `lib/music_library_web/controllers/error_json.ex` | Renders error/occurrence data |
| Router | `lib/music_library_web/router.ex` | `/api/v1/errors` routes in API scope (Bearer token auth) |
| Auth | `lib/music_library_web/auth.ex` | `require_api_token/2` plug — Bearer token comparison |
### Pi Extension (TypeScript)
| Component | File | Status |
|---|---|---|
| Component | File | Status |
| --------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------- |
| prod-errors extension | `.pi/extensions/prod-errors/index.ts` | Tools: `fetch_production_errors`, `fetch_production_error`; Command: `/prod-errors` (TUI browser) |
| TUI ErrorBrowser | Same file, `ErrorBrowser` class | List view (no mute/resolve actions), Detail view (no mute/resolve actions) |
| TUI ErrorBrowser | Same file, `ErrorBrowser` class | List view (no mute/resolve actions), Detail view (no mute/resolve actions) |
### Gap Analysis
@@ -48,12 +49,14 @@ POST /api/v1/errors/:id/resolve → sets status = :resolved
```
**Elixir changes:**
- `MusicLibrary.Errors`: Add `mute_error/1`, `resolve_error/1` (or a shared private helper)
- `MusicLibraryWeb.ErrorController`: Add `mute/2` and `resolve/2` actions
- `MusicLibraryWeb.ErrorJSON`: Add `mute/1`, `resolve/1` render functions
- `MusicLibraryWeb.Router`: Add two POST routes in the API scope
**Pi extension changes:**
- Register `mute_production_error` tool → POSTs to `/api/v1/errors/:id/mute`
- Register `resolve_production_error` tool → POSTs to `/api/v1/errors/:id/resolve`
- Add keybindings to `/prod-errors` TUI:
@@ -62,6 +65,7 @@ POST /api/v1/errors/:id/resolve → sets status = :resolved
- Or use single keys: `x` for mute, `d` for resolve (avoiding conflict with existing `m`/`r` filter toggles)
**Pros:**
- Simplest approach — each endpoint does exactly one thing
- RESTful resource/action pattern used elsewhere in the app (e.g., `/collection/latest`)
- Easy to extend with unmute/unresolve later as additional endpoints
@@ -69,6 +73,7 @@ POST /api/v1/errors/:id/resolve → sets status = :resolved
- Easy to test independently
**Cons:**
- Two new routes instead of one
- If many more error actions are added, route count could grow
@@ -81,11 +86,13 @@ PATCH /api/v1/errors/:id body: {"muted": true} or {"status": "resolved"}
```
**Pros:**
- Single route for all error mutations
- Follows REST convention for partial updates
- Easily extensible to other fields
**Cons:**
- Parameter validation is more complex (must reject unknown fields, validate combinations)
- Tool semantics are less clear — the pi tool would need to construct the PATCH body
- PATCH semantics imply partial update; if both fields are sent, behavior must be defined
@@ -99,12 +106,14 @@ POST /api/v1/errors/:id/actions body: {"action": "mute"} or {"action": "resolv
```
**Pros:**
- Single route for all actions
- Easy to add new action types
- Clean separation between action specification and execution
- Tool semantics map cleanly (tool name = action)
**Cons:**
- Less standard REST pattern
- Requires action validation/dispatch layer
- No existing precedent in the codebase
@@ -125,16 +134,16 @@ Route A is the best fit for this project because:
## Architecture Impact
| Component | Impact |
|---|---|
| `MusicLibrary.Errors` | Add `mute_error/1`, `resolve_error/1` context functions |
| `MusicLibraryWeb.ErrorController` | Add `mute/2`, `resolve/2` actions |
| `MusicLibraryWeb.ErrorJSON` | Add render functions for success/error responses |
| `MusicLibraryWeb.Router` | Add two POST routes |
| `.pi/extensions/prod-errors/index.ts` | Add two tools, add TUI keybindings |
| `test/music_library_web/controllers/error_controller_test.exs` | Add tests for new endpoints |
| `test/music_library/errors_test.exs` | Add tests for new context functions (if separate test file exists) |
| `docs/architecture.md` | Update Routes section if it lists API routes |
| Component | Impact |
| -------------------------------------------------------------- | ------------------------------------------------------------------ |
| `MusicLibrary.Errors` | Add `mute_error/1`, `resolve_error/1` context functions |
| `MusicLibraryWeb.ErrorController` | Add `mute/2`, `resolve/2` actions |
| `MusicLibraryWeb.ErrorJSON` | Add render functions for success/error responses |
| `MusicLibraryWeb.Router` | Add two POST routes |
| `.pi/extensions/prod-errors/index.ts` | Add two tools, add TUI keybindings |
| `test/music_library_web/controllers/error_controller_test.exs` | Add tests for new endpoints |
| `test/music_library/errors_test.exs` | Add tests for new context functions (if separate test file exists) |
| `docs/architecture.md` | Update Routes section if it lists API routes |
No changes needed to: supervision tree, PubSub, schemas, migrations, Oban workers, LiveViews, external APIs.
@@ -156,10 +165,10 @@ No production infrastructure changes needed. The new endpoints are served by the
## Documentation Updates
| File | Change |
|---|---|
| `docs/architecture.md` | Update Routes section if API routes are enumerated there |
| README (if applicable) | N/A — API is internal tooling |
| File | Change |
| ------------------------------------- | -------------------------------------------------------------- |
| `docs/architecture.md` | Update Routes section if API routes are enumerated there |
| README (if applicable) | N/A — API is internal tooling |
| `.pi/extensions/prod-errors/index.ts` | Self-documenting through tool descriptions and prompt snippets |
## Testing