Skip to content

feat(deployer): display monitored application logo - #295

Merged
thiagoesteves merged 8 commits into
mainfrom
thiagoesteves/application-favicon
Aug 13, 2026
Merged

feat(deployer): display monitored application logo#295
thiagoesteves merged 8 commits into
mainfrom
thiagoesteves/application-favicon

Conversation

@thiagoesteves

@thiagoesteves thiagoesteves commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add Deployer.Status.Logo, which reads priv/static/images/logo.svg from a monitored application's own priv directory over RPC and returns it as a base64 data: URI.
  • Add a logo field to Deployer.Status and Deployer.Status.Application, cached per node and cleared when the node disconnects.
  • Update ApplicationCard to display the application logo, falling back to the generic language icon.

Notes

Lookup is best effort and never prevents an application from rendering. It separates two cases that matter to the caller: a node that answered and has no logo.svg returns {:ok, nil} and is cached, while an unknown application name or a node that did not reply returns :error and is retried. The per node cache only stores a value on {:ok, value}, so a transient RPC failure is not remembered as a missing logo.

The card icon does not use the avatar component. Its img rule sets object-fit: cover at a higher specificity than the utility classes, and both land in the same cascade layer, so a non-square logo was cropped rather than fitted. A plain flex box with object-contain scales any logo to fit without distortion.

The feature is named logo rather than favicon throughout, since it reads the application's logo.svg and never a favicon. The browser's own favicon.ico static path is unchanged.

Test plan

  • mix compile --warnings-as-errors
  • mix format --check-formatted
  • mix credo --strict
  • mix dialyzer
  • mix test (full umbrella suite green)
  • New Deployer.Status.LogoTest covers the success path, the exact priv path requested from the node, a missing file, an application that is not loaded, an unreachable node, an unknown application name that must not reach the node, and the split between a definitive miss and a node that did not answer
  • Logo rendering verified visually in the running dashboard

Risk assessment

  • Impact: Monitored applications shipping priv/static/images/logo.svg show their own logo in the dashboard; everything else keeps the existing language icon.
  • Blast radius: Deployer.Status, Deployer.Status.Application, Deployer.Status.Logo, DeployexWeb.Components.ApplicationCard.
  • Regression risk: Low. The lookup is read only, bounded by an RPC timeout, and every failure path falls back to the language icon. Applications shipping only favicon.ico are not picked up, matching the previous behaviour for apps without an icon.
  • Rollback: Revert the commits.

🤖 Generated with Claude Code

Add Deployer.Status.Favicon to read a monitored Phoenix endpoint's
priv/static/favicon.ico via RPC and return it as a base64 data URI.
Add the favicon field to Deployer.Status and populate it in
Deployer.Status.Application, falling back to the language icon in the
ApplicationCard when no favicon is found.

Risk assessment:
- Impact: The dashboard shows each monitored app's own favicon instead of
  the generic Elixir/Erlang/Gleam language icon.
- Blast radius: Deployer.Status, Deployer.Status.Application,
  Deployer.Status.Favicon, and the ApplicationCard component.
- Regression risk: Low. Missing or unreachable favicons fall back to the
  existing language icon.
- Rollback: Revert this commit.
The dashboard card is too large for a 16x16/32x32 favicon, so look for
a logo.svg first and only fall back to favicon.ico when no logo exists.
Instead of discovering the endpoint's BEAM path, use the monitored
application's name to call :code.priv_dir/1 and then search for
logo.* and favicon.* under priv and priv/* (e.g. priv/static).
- Use String.to_existing_atom for the main app name and String.to_atom
  for the common _web umbrella variant.
- Build wildcard patterns as ~c charlists for filelib:wildcard.
- Search both :code.priv_dir(app) and :code.priv_dir(\"#{app}_web\") so
  umbrella apps with a separate web app are also covered.
- Remove the _web umbrella fallback and rely on the monitored app name.
- Use String.to_existing_atom instead of creating atoms for guessed names.
- Search only inside priv/static/images for logo.* and favicon.*.
…icon

Replace the wildcard icon discovery with a direct lookup of
`priv/static/images/logo.svg` in the monitored application's own priv
directory, resolved over RPC. This drops the pattern matching, scoring
and MIME sniffing helpers in favour of a single known path and a fixed
`image/svg+xml` data URI.

Unknown application names are now treated as a normal miss instead of
raising, and `image/2` always answers `{:ok, logo_or_nil}`, so the spec
and docs no longer advertise an unreachable `:error` return.

In the card, the icon no longer uses the avatar component. Its `img`
rule sets `object-fit: cover` at a higher specificity than the utility
classes and lands in the same cascade layer, so a non-square logo was
cropped instead of being fitted. A plain flex box with `object-contain`
scales any logo to fit without distortion.

## Risk assessment
- **Impact:** Monitored applications shipping `priv/static/images/logo.svg`
  show their own logo; everything else falls back to the language icon.
- **Blast radius:** The favicon lookup module and the application card.
- **Regression risk:** Low. Applications that only ship `favicon.ico` lose
  the icon and fall back to the language icon, which is the previous
  behaviour for apps without any icon.
- **Rollback:** Revert the commit.
@thiagoesteves thiagoesteves changed the title feat(deployer): display monitored application favicon feat(deployer): display monitored application logo Aug 13, 2026
The module never read a favicon: it reads the application's `logo.svg`.
Rename `Deployer.Status.Favicon` to `Deployer.Status.Logo` and the
`favicon` field on the status struct to `logo`, along with the process
cache key and the card assign, so the name matches what is fetched. The
browser's own `favicon.ico` static path is untouched.

Add unit tests for `Logo.image/2` covering the success path, the exact
priv path requested from the node, a missing file, an application that is
not loaded, an unreachable node, and an unknown application name that
must not reach the node at all.

## Risk assessment
- **Impact:** None at runtime. A field and a module are renamed together
  with their only call sites.
- **Blast radius:** The logo lookup module, the status struct and the
  application card.
- **Regression risk:** Low. The rename is mechanical, the full suite
  passes, and the new tests pin the lookup behaviour.
- **Rollback:** Revert the commit.
@thiagoesteves thiagoesteves self-assigned this Aug 13, 2026
…no answer

Narrowing `Logo.image/2` to always return `{:ok, value}` made the
non-ok branch of the process cache unreachable, which dialyzer reported
as an uncoverable pattern.

The branch is load bearing rather than dead: the cache only stores a
value on `{:ok, value}`, so collapsing every failure into `{:ok, nil}`
made an unreachable node cache a missing logo and stop retrying for the
lifetime of the cache entry.

Restore the distinction. A node that answered and has no `logo.svg`
returns `{:ok, nil}` and is cached, while an unknown application name or
a node that did not reply returns `:error` and is retried. Both branches
of the cache are reachable again and dialyzer passes.

## Risk assessment
- **Impact:** An unreachable node no longer sticks to the language icon
  once it comes back within the same cache window.
- **Blast radius:** The logo lookup module and its call site in the
  status application.
- **Regression risk:** Low. Rendering is unchanged in every case, since
  both returns already fall back to the language icon. Covered by tests
  and dialyzer is clean.
- **Rollback:** Revert the commit.
@thiagoesteves
thiagoesteves merged commit aca3547 into main Aug 13, 2026
3 checks passed
@thiagoesteves
thiagoesteves deleted the thiagoesteves/application-favicon branch August 13, 2026 19:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant