Journey
Some slices are about building new things. This one was about fixing what broke and laying the foundation for what’s next.
On May 28, Clankies had a bug slam — four issues reported and fixed in a single afternoon session. The avatar editor wiped form fields, the Power-Up button did nothing when clicked, agents went silent when no API key was configured, and toast messages had a weird pinkish ghost background. Each bug had a clear root cause, a targeted one-file fix, and all four were reviewed, approved, and deployed within hours.
Then the Trello Power-Up integration — designed in ADR-004 and sketched out in the previous slice — got its full backend implementation. Seven modules across the Trello boundary context, 1,634 lines of production code, and 1,424 lines of tests covering OAuth token exchange, webhook lifecycle, card/board API operations, and the agent bridge. The architecture decisions are documented, the test suite is comprehensive, and the config UI (slice 03) now has a real backend to talk to.
A Brevo SMTP regression — tls: :always instead of tls: :if_available on port 587 — was caught and fixed before it could break production email delivery again.
Here’s what happened.
What Changed
1. Bug Slam: Four Bugs, One Afternoon
Four issues hit the kanban in quick succession on May 28. The flash agent fixed all of them in under an hour of wall-clock time, with the code-reviewer profile reviewing and unblocking each one.
Bug 1: Changing Clanky’s avatar wiped all other form fields
Symptom: Clicking an emoji in the avatar selector caused every input field (name, description, system prompt) to reset to empty values.
Root cause: The agent config form used for={%{}} — an empty map — instead of an Ecto changeset. Without a changeset-backed form, LiveView’s form recovery mechanism had nothing to recover from. When phx-click on the emoji triggered a server re-render, DOM patching replaced all <input> elements, and the user’s typed-in values vanished.
Fix: Four changes in agent_live.ex:
# Before: form had no changeset — no form recovery
<.form for={%{}} id="agent-config-form" phx-submit="update_agent">
# After: changeset-backed form with automatic form recovery
# mount assigns:
config_changeset: Agents.change_agent(agent)
# select_config_avatar updates the changeset:
socket = assign(socket, :config_changeset,
Ecto.Changeset.put_change(socket.assigns.config_changeset, :emoji, emoji))
# form template:
<.form for={@config_changeset} id="agent-config-form" phx-submit="update_agent">
LiveView’s built-in form recovery works by comparing server-assigned changeset values with client-side <input> values. When the emoji changes, only that field gets patched; the rest stay intact.
Bug 2: “Add Power-Up” button did nothing when clicked
Symptom: Clicking the “+ Add Power-Up” button showed no visible response. The dropdown menu never appeared.
Root cause: The phx-click attribute toggled the open CSS class on #powerup-menu — the child .dropdown-content div. But the DaisyUI CSS selector .dropdown.open .dropdown-content expects open on the parent .dropdown container. The class was being added to the wrong element.
Fix: Added id="powerup-dropdown" to the parent container and changed the toggle target:
# Before: toggling open on the content div
<div class="dropdown mt-3">
<button phx-click={JS.toggle_class("open", to: "#powerup-menu")}>
<div id="powerup-menu" class="dropdown-content">
# After: toggling open on the parent container
<div class="dropdown mt-3" id="powerup-dropdown">
<button phx-click={JS.toggle_class("open", to: "#powerup-dropdown")}>
<div class="dropdown-content">
A one-character fix (well, two lines) for something that visibly blocks the entire Power-Up workflow.
Bug 3: Agents not responding in chat
Symptom: Users sent messages to their agent, saw a brief typing indicator flicker, then silence. The message appeared sent but no response arrived — as if the agent was ignoring the user.
Root cause: The user_api_keys table was empty. No one had configured a DeepSeek API key. The agent code correctly detected this (get_adapter_config returned api_key: nil), but the error was displayed only as a flash notification — which auto-dismisses after a few seconds. By the time the user looked at the chat area, the flash was gone and they saw only the empty chat.
Fix: Modified handle_info({:error, reason}, socket) in agent_live.ex to add the error as a persistent inline assistant message in the chat area. Now users clearly see “API key not configured — go to Settings to add one” instead of confusing silence.
Bug 4: Pinkish ghost background on toast alerts
Symptom: Toast/flash messages had a strange pinkish color that looked like text with a background highlight, rather than a proper alert background.
Root cause: The flash component rendered an outer .toast-message div with background: var(--n) (dark neutral) wrapping an inner div with .alert-error class adding its own background: color-mix(in oklch, var(--er) 10%, var(--b1)) (light pink). Two overlapping backgrounds created a visual artifact — the pink appeared as a separate patch behind the text instead of filling the entire toast.
Fix: Removed background, color, and padding from the outer .toast-message and moved padding (p-3) to the inner div. The inner .alert-* div now provides the sole background.
2. Brevo SMTP STARTTLS Fix
A one-line change in config/runtime.exs that took three commits to get right.
The problem: Brevo’s SMTP relay on port 587 uses STARTTLS — the connection starts plaintext and upgrades to TLS via the STARTTLS command. Setting tls: :always in the Swoosh/SMPT config causes gen_smtp to expect a TLS handshake from the first byte, which Brevo doesn’t provide on port 587. The result: {:error, {:retries_exceeded, {:temporary_failure, "smtp-relay.brevo.com", :tls_failed}}}.
The fix: tls: :if_available — which lets gen_smtp start with plaintext, negotiate STARTTLS if the server advertises it, and upgrade the connection.
# Before (wrong for Brevo on port 587):
config :clankies, Clankies.Mailer,
adapter: Swoosh.Adapters.SMTP,
relay: smtp_relay,
port: 587,
tls: :always # ← expects TLS from byte 1
# After (correct):
config :clankies, Clankies.Mailer,
adapter: Swoosh.Adapters.SMTP,
relay: smtp_relay,
port: 587,
tls: :if_available # ← STARTTLS negotiation
The meta-bug: This was originally fixed in commit f09a540, but the flash agent’s Trello backend work touched runtime.exs and silently reverted the fix to tls: :always in commit 4196494. A git log -- config/runtime.exs before deploy would have caught it. The project now has a deploy checklist item: always check for reverted config fixes.
3. Trello Backend Architecture (ADR-004)
The Trello integration backend is built as three layered components, documented in ADR-004:
sequenceDiagram
participant User as Browser User
participant Trello as Trello API
participant Auth as Auth Module
participant Webhook as EventProcessor
participant Agent as AgentServer
Note over User,Agent: OAuth Flow
User->>Trello: GET /1/authorize (redirect)
Trello->>User: Token returned
User->>Auth: handle_callback(token)
Auth->>Trello: GET /1/members/me (validate)
Trello-->>Auth: {id, username}
Auth->>DB: upsert TrelloConnection
Auth-->>User: ✅ Connected
Note over User,Agent: Webhook Event Flow
Trello->>Webhook: POST /api/trello/webhook
Webhook->>Webhook: Dedup by action.id (ETS)
Webhook->>Webhook: Filter self-triggered events
Webhook->>Webhook: Format event → agent message
Webhook->>Agent: send_message(board_action)
Agent-->>Webhook: async processing
Webhook-->>Trello: 200 OK
Note over User,Agent: Agent Tool Flow
Agent->>ApiClient: trello_get_card(card_id)
ApiClient->>Trello: GET /1/cards/:id
Trello-->>ApiClient: {id, name, desc, ...}
ApiClient-->>Agent: formatted card data
Seven backend modules, one clear boundary:
| Module | Lines | Responsibility |
|---|---|---|
Clankies.Trello.Auth |
133 | OAuth token exchange, validation via Trello API |
Clankies.Trello.ApiClient |
212 | Req-based Trello REST client, test plug injection |
Clankies.Trello.EventProcessor |
187 | Webhook dedup (ETS), self-filtering, async dispatch |
Clankies.Trello.WebhookRegistrar |
63 | Webhook lifecycle (register/deregister) |
Clankies.Trello.TrelloConnection |
47 | Ecto schema: stored Trello OAuth tokens |
Clankies.Trello.TrelloBoardMapping |
34 | Ecto schema: board-to-agent link |
Clankies.Trello (context) |
153 | CRUD operations, webhook orchestration |
Total: 1,634 lines of backend code + 1,424 lines of tests across 10 test files.
Key architectural decisions from ADR-004:
- API Key + Token auth (not OAuth 2.0) — Trello’s native authorization model; token can be set to never expire
- ETS-based webhook dedup — simple, fast, avoids DB round-trips for the common case; 5-minute TTL
- Async webhook processing —
Task.startto return 200 OK immediately, then process asynchronously - Test-mode plug injection —
Application.get_env(:clankies, :trello_test_plug)lets tests stub Trello API responses via a local Plug instead of hitting the real API - Keyword-list AND map params —
ApiClient.request/4handles both param styles without crashing
# ApiClient.request/4 now handles both param styles
defp merge_params(params, api_key, trello_connection) do
case params do
list when is_list(list) ->
Keyword.merge(list, key: api_key, token: trello_connection.access_token)
map when is_map(map) ->
Map.merge(map, %{key: api_key, token: trello_connection.access_token})
end
end
# Webhook dedup via ETS — fast, simple, no DB query
defp deduplicate(action_id) do
now = System.system_time(:second)
case :ets.lookup(@ets_table, action_id) do
[{^action_id, _timestamp}] ->
Logger.debug("Dropping duplicate Trello webhook: #{action_id}")
:duplicate
[] ->
:ets.insert(@ets_table, {action_id, now + @dedup_ttl_minutes * 60})
:ok
end
end
4. Trello Config UI Refinements (Uncommitted)
The Trello Settings LiveView gained several UX improvements that are in working-tree changes awaiting review:
- Auto-load boards — when the page loads with an active Trello connection, boards are fetched automatically after 100ms
- Board search — text input with client-side filtering by board name (case-insensitive substring match)
- Map confirmation animation — a subtle
pulse-oncekeyframe (two beats of 1.02x scale) draws the user’s eye to the newly-mapped board card - Error handler expansion — the
map_boardhandler now catches{:error, reason}whenreasonis a string (not just Ecto changeset errors) - State cleanup — many event handlers now clear stale assigns (
mapped_confirmation,search_query) to prevent visual bugs
# Auto-load boards on mount (if already connected)
if connection do
Process.send_after(self(), :auto_load_boards, 100)
end
# Board search with case-insensitive filtering
defp filtered_boards(boards, query) do
query_lower = String.downcase(query)
Enum.filter(boards, fn board ->
String.contains?(String.downcase(board.name), query_lower)
end)
end
/* Pulse-once confirmation animation */
@keyframes pulse-once {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.02); }
}
.animate-pulse-once {
animation: pulse-once 0.6s ease-in-out 2;
}
Architecture
The Trello integration follows the project’s existing pattern: a boundary context (lib/clankies/trello/) with its own modules, schemas, and context module, plus controller/LiveView layers in the web layer. The full data flow from user action to Trello API to agent response:
graph TD
subgraph Browser
U[User]
LV[TrelloSettingsLive]
end
subgraph Web
TC[TrelloController]
TAC[TrelloApiController]
TWC[TrelloWebhookController]
end
subgraph Domain
A[Auth<br/>OAuth flow]
AC[ApiClient<br/>Req-based Trello client]
EP[EventProcessor<br/>ETS dedup + format]
WR[WebhookRegistrar<br/>Lifecycle mgmt]
TX[Trello context<br/>CRUD + orchestration]
end
subgraph Data
TC_s[TrelloConnection]
TBM[TrelloBoardMapping]
end
subgraph Agent
AS[AgentServer<br/>message dispatch]
PU[PowerUp module<br/>tools + prompts]
end
U -->|Settings| LV
LV -->|Connect| TC
TC -->|Authorize| A
A -->|Validate token| AC
AC -->|GET /1/members/me| TrelloAPI
A -->|Upsert| TC_s
LV -->|Load boards| TAC
TAC -->|List boards| AC
AC -->|GET /1/members/me/boards| TrelloAPI
LV -->|Map board| TAC
TAC -->|Create mapping| TX
TX -->|Register webhook| WR
WR -->|POST /1/webhooks| TrelloAPI
WR -->|Store webhook_id| TBM
TrelloAPI -->|Event| TWC
TWC -->|Process| EP
EP -->|Route to agent| AS
AS -->|Execute tool| PU
PU -->|API call| AC
AC -->|GET/POST /1/*| TrelloAPI
style TrelloAPI fill:#0079BF,color:#fff
style PU fill:#6C5CE7,color:#fff
The integration is structured as a Trello context module (Clankies.Trello) that orchestrates the sub-modules. This keeps the web layer thin: controllers call the context, the context manages the data and external API interactions.
Debugging
| Bug | Symptom | Root Cause | Fix |
|---|---|---|---|
| Avatar form wipes fields | Changing emoji resets all inputs | for={%{}} instead of changeset disables form recovery |
Changeset-backed form with Ecto.Changeset.put_change |
| Power-Up button silent | Clicking button shows no menu | open class toggled on child .dropdown-content, not parent .dropdown |
Updated phx-click target to parent container |
| Agent not responding | Message sent, typing indicator, then silence | API key missing; error shown as flash (auto-dismisses) not inline message | Error bubbles now persist as chat messages |
| Pinkish toast ghost | Alert text looks highlighted pink | Outer toast-message background: var(--n) overlaps inner .alert-error pink bg |
Removed outer bg; inner div provides full bg |
| Brevo email failure | Emails not delivered | tls: :always on port 587 expects TLS from byte 1; Brevo uses STARTTLS |
Changed to tls: :if_available |
| map_board crash | LiveView error on board mapping | Handler only matched {:error, %Ecto.Changeset{}}, not {:error, "string"} |
Added {:error, reason} clause |
Technical Decisions
| Decision | Rationale | Trade-offs |
|---|---|---|
| Changeset-backed form | LiveView’s form recovery is built-in and zero-config | Requires discipline to keep changeset in sync across events |
| ETS webhook dedup | Fast, no DB query, survives only in-memory | State lost on restart; 5-min TTL is arbitrary |
| Async webhook processing | Returns 200 OK immediately; Trello retries on timeout | Error handling is decoupled from the request lifecycle |
| Test plug injection | Clean test stubbing without mock server | App config coupling; prod path has conditional branch |
tls: :if_available |
STARTTLS negotiation for Brevo on port 587 | Less secure if connection downgrade is possible (mitigated by enforcing port 587) |
What We Learned
- One bad CSS selector can break an entire feature. The Power-Up button wasn’t just “not working” — it was working perfectly on the wrong DOM element. Always check which element carries the toggle class.
- Flash notifications are silent failures. If an error auto-dismisses in 3 seconds and the user isn’t staring at the toast, they’ll think the app is broken. Persistent inline messages are the only reliable way to show errors.
- Config regressions are hard to catch automatically. The
flashagent works fast and doesn’t read git history. Alwaysgit log -- config/runtime.exsbefore deploy — the project now has this as a deploy checklist item. - ETS is perfect for webhook dedup. Trello sends at-least-once events. A named ETS table with a 5-minute TTL is simpler and faster than a DB query, and the trade-off (lost on restart) is acceptable for webhook dedup.
What’s Next
The Trello backend is complete and tested. The board selection UI has two child tasks ready for the flash agent: fix 8 failing LiveView tests (Slice 1a) and polish the board loading UX with refresh, error recovery, empty states, and skeleton loading (Slice 1b). After that, the final stretch: the list/label mapping UI and end-to-end integration testing.