feat(deployer): display monitored application logo - #295
Merged
Conversation
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.
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.
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Deployer.Status.Logo, which readspriv/static/images/logo.svgfrom a monitored application's ownprivdirectory over RPC and returns it as a base64data:URI.logofield toDeployer.StatusandDeployer.Status.Application, cached per node and cleared when the node disconnects.ApplicationCardto 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.svgreturns{:ok, nil}and is cached, while an unknown application name or a node that did not reply returns:errorand 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
imgrule setsobject-fit: coverat 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 withobject-containscales any logo to fit without distortion.The feature is named
logorather thanfaviconthroughout, since it reads the application'slogo.svgand never a favicon. The browser's ownfavicon.icostatic path is unchanged.Test plan
mix compile --warnings-as-errorsmix format --check-formattedmix credo --strictmix dialyzermix test(full umbrella suite green)Deployer.Status.LogoTestcovers 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 answerRisk assessment
priv/static/images/logo.svgshow their own logo in the dashboard; everything else keeps the existing language icon.Deployer.Status,Deployer.Status.Application,Deployer.Status.Logo,DeployexWeb.Components.ApplicationCard.favicon.icoare not picked up, matching the previous behaviour for apps without an icon.🤖 Generated with Claude Code