17 KiB
id, title, status, assignee, created_date, updated_date, labels, dependencies, references, modified_files, priority, ordinal
| id | title | status | assignee | created_date | updated_date | labels | dependencies | references | modified_files | priority | ordinal | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ML-165 | Mute and resolve production errors from pi | Done | 2026-05-05 11:20 | 2026-05-05 12:27 |
|
|
medium | 10000 |
Description
Extend the production error tooling in pi so that it's possible to:
- Mute and resolve issues from the
prod-errorsTUI (user action) - 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.
Acceptance Criteria
- #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
Implementation Plan
Implementation Plan: Route A — Four dedicated POST endpoints
Overview
Add four API endpoints (/mute, /unmute, /resolve, /unresolve) under POST /api/v1/errors/:id/, matching context functions in MusicLibrary.Errors, controller actions in MusicLibraryWeb.ErrorController, four pi tools, and TUI keybindings in the /prod-errors browser.
Step 1: Add context functions (MusicLibrary.Errors)
File: lib/music_library/errors.ex
Add four public functions and one private helper:
@spec mute_error(pos_integer()) :: {:ok, Error.t()} | {:error, :not_found | Ecto.Changeset.t()}
def mute_error(id), do: update_error_field(id, :muted, true)
@spec unmute_error(pos_integer()) :: {:ok, Error.t()} | {:error, :not_found | Ecto.Changeset.t()}
def unmute_error(id), do: update_error_field(id, :muted, false)
@spec resolve_error(pos_integer()) :: {:ok, Error.t()} | {:error, :not_found | Ecto.Changeset.t()}
def resolve_error(id), do: update_error_field(id, :status, :resolved)
@spec unresolve_error(pos_integer()) :: {:ok, Error.t()} | {:error, :not_found | Ecto.Changeset.t()}
def unresolve_error(id), do: update_error_field(id, :status, :unresolved)
defp update_error_field(id, field, value) do
with %Error{} = error <- Repo.get(Error, id),
{:ok, updated} <- error |> Ecto.Changeset.change([{field, value}]) |> Repo.update() do
{:ok, updated}
else
nil -> {:error, :not_found}
{:error, changeset} -> {:error, changeset}
end
end
Place public functions after existing get_error/1, private helper at bottom with other private functions.
Verification: mix test test/music_library/errors_test.exs (after adding tests in Step 5)
Step 2: Add controller actions (MusicLibraryWeb.ErrorController)
File: lib/music_library_web/controllers/error_controller.ex
Add four actions (mute/2, unmute/2, resolve/2, unresolve/2). Each:
- Parses the
idparam viaInteger.parse/1 - Calls the corresponding context function
- Renders the updated error on success, returns 404/422 on failure
def mute(conn, %{"id" => id}), do: perform_action(conn, id, &Errors.mute_error/1)
def unmute(conn, %{"id" => id}), do: perform_action(conn, id, &Errors.unmute_error/1)
def resolve(conn, %{"id" => id}), do: perform_action(conn, id, &Errors.resolve_error/1)
def unresolve(conn, %{"id" => id}), do: perform_action(conn, id, &Errors.unresolve_error/1)
defp perform_action(conn, id, action_fn) do
case Integer.parse(id) do
{id_int, ""} when id_int > 0 ->
case action_fn.(id_int) do
{:ok, error} ->
render(conn, :update, error: error)
{:error, :not_found} ->
conn |> put_status(:not_found) |> json(%{error: "Not Found"})
{:error, _changeset} ->
conn |> put_status(:unprocessable_entity) |> json(%{error: "Update failed"})
end
_ ->
conn |> put_status(:not_found) |> json(%{error: "Not Found"})
end
end
Verification: mix test test/music_library_web/controllers/error_controller_test.exs (after adding tests in Step 5)
Step 3: Add JSON render function
File: lib/music_library_web/controllers/error_json.ex
Add an update/1 render function that returns the updated error (Phoenix maps the template atom :update to the function update/1):
def update(%{error: error}) do
%{error: error(error)}
end
Place near the existing show/1 function.
Verification: Controller tests will exercise this indirectly; the rendered JSON should contain the updated error fields reflecting the mutation that was performed.
Step 4: Add routes
File: lib/music_library_web/router.ex
Add four POST routes inside the scope "/api/v1" block, after the existing error GET routes:
post "/errors/:id/mute", ErrorController, :mute
post "/errors/:id/unmute", ErrorController, :unmute
post "/errors/:id/resolve", ErrorController, :resolve
post "/errors/:id/unresolve", ErrorController, :unresolve
Verification: mix test test/music_library_web/controllers/error_controller_test.exs (auth tests should verify 401 for all four)
Step 5: Add tests
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/1setsmutedtotrueunmute_error/1setsmutedtofalseresolve_error/1setsstatusto:resolvedunresolve_error/1setsstatusto:unresolved- Returns
{:error, :not_found}for non-existent ID - Idempotency for all four actions: calling mute on already-muted error succeeds (no-op), unmute on already-unmuted succeeds, resolve on already-resolved succeeds, unresolve on already-unresolved succeeds
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
- Each endpoint returns 404 for non-integer ID
Verification: mix test test/music_library/errors_test.exs test/music_library_web/controllers/error_controller_test.exs — all tests pass.
Step 6: Add pi tools
File: .pi/extensions/prod-errors/index.ts
Register four new tools after the existing fetch_production_error tool:
mute_production_error— POSTs to/api/v1/errors/:id/muteunmute_production_error— POSTs to/api/v1/errors/:id/unmuteresolve_production_error— POSTs to/api/v1/errors/:id/resolveunresolve_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
- Returns a success message with the updated error in details, or an error message
- Has appropriate
promptSnippetandpromptGuidelines
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."unresolve_production_error: "Use unresolve_production_error to reopen a production error when it reoccurs after being resolved."
Verification: In a pi session, call mute_production_error with a known error ID — verify response shows success. Repeat for the other three tools.
Step 7: Add TUI keybindings
File: .pi/extensions/prod-errors/index.ts
Add to the ErrorBrowser class:
-
New methods:
toggleMute(id, currentMuted)— POSTs to mute or unmute endpoint, updates local error state on success, shows notification on failuretoggleResolve(id, currentStatus)— POSTs to resolve or unresolve endpoint, updates local error state on success, shows notification on failure
-
New keybindings in
handleInput:M(shift+m) in list mode: callstoggleMuteon the selected errorR(shift+r) in list mode: callstoggleResolveon the selected errorMin detail mode: callstoggleMuteon the displayed errorRin detail mode: callstoggleResolveon the displayed error
Lowercase
mandrremain the existing filter toggles (muted filter, resolved filter). -
Update help text in
renderListandrenderDetailto 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
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.
Verification: Run /prod-errors in pi, navigate to an error, press M — verify the muted state toggles and the [MUTED] label appears/disappears on the error line. Press R — verify the resolved status badge toggles. In detail mode, verify the same keys work and the header updates. Verify help text shows the new keys.
Step 8: Update documentation
File: lib/music_library/errors.ex
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 viaErrorTracker.ErrorNotifier(which checks themutedfield before dispatching). - Under the Controller table, update the
ErrorControllerrow to include the four new action routes.
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 |
No changes: supervision tree, PubSub, schemas, migrations, Oban workers, LiveViews, external APIs, production infrastructure.
Interaction with ErrorTracker.ErrorNotifier: The existing notifier (supervised in the application) already checks error.muted before dispatching email notifications. Muting an error via this API will therefore immediately suppress future email alerts for that error — this is the desired behavior and is consistent with how the ErrorTracker web dashboard's mute button works.
Performance
- DB: Single-row UPDATE by primary key → O(1), ~1–5ms in WAL-mode SQLite
- No N+1 risk: No joins, no preloads
- HTTP: Minimal JSON encoding overhead
- Concurrency: SQLite serializes writes — negligible for low-frequency admin actions
Cost
Zero incremental cost. No external API calls.
Production Infrastructure
No changes needed (no new env vars, no DNS, no firewall, no special migration handling).
Final Summary
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 toErrorTracker's built-in mutation functions.resolve/1andunresolve/1handle the idempotent case (already-resolved/already-unresolved) gracefully.MusicLibraryWeb.ErrorController: Added 4 POST actions (mute/2,unmute/2,resolve/2,unresolve/2) with a sharedperform_action/3helper that parses integer IDs, delegates to context, and returns proper JSON responses (200, 404, 422).MusicLibraryWeb.ErrorJSON: Addedupdate/1render function for the:updatetemplate atom.MusicLibraryWeb.Router: Added 4 POST routes under the authenticated/api/v1scope.
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@moduledocto reflect mutation capabilities.
Design decisions
- Used
ErrorTracker.mute/1,unmute/1,resolve/1,unresolve/1(which emit telemetry events) rather than rawEcto.Changeset.change/2 resolve_error/1andunresolve_error/1handle 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/1render helper